d3.js drawing circles with throbbing outer lines - javascript

I am trying to create something similar to this
http://demo.joostkiens.com/het-parool-4g/
Where the plotted circles have outer lines throbbing outwards.
This is my demo so far.
http://jsfiddle.net/NYEaX/95/
I've plotted the circles with some dummy data. On top are red based circles. How do I invoke the animation and make it more vibrant per the alarm data .. eg. alarmLevel.
I am unsure as to how to create a looping animation with the radius bouncing off past the circumference and then fading out - having this variety based on the alarmLevel threshold
Would ideally need the transition to occur like this in a loop., http://jsfiddle.net/pnavarrc/udMUx/
var speedLineGroup = sampleSVG.append("g")
.attr("class", "speedlines");
speedLineGroup.selectAll("circle.dl-speed-static")
.data(dataset)
.enter().append("circle")
.style("stroke", "red")
.style("fill", "black")
.attr("r", function(d){
return d.value;
})
.attr("cx", function(d){
return d.xcoord;
})
.attr("cy", function(d){
return d.ycoord;
})
.attr("class", "dl-speed-static")
.attr("stroke-opacity", function (e) {
return 1;
//return var
})
.attr("fill-opacity", function (e) {
return 1;
//return var
})
.transition()
.ease("linear")
.duration(200)
.attr("r", function (e) {
return 1;
//return var
})
I've merged the ideas in the post. I've placed the ring creation in its own function and removed the time out. I've also started to try and hook into the alarm threshold per marker.
http://jsfiddle.net/NYEaX/102/
but the application still seems delayed/buggy - not very clear as in the prime example. How could this be improved further. Some of the alarm counts are low - but this method is causing the ring to throb too soon or flickery. Its almost like I need to invert the value to have a low alarm - create a slower response.
function makeRings() {
var datapoints = circleGroup.selectAll("circle");
var radius = 1;
function myTransition(circleData){
var transition = d3.select(this).transition();
speedLineGroup.append("circle")
.attr({"class": "ring",
"fill":"red",
"stroke":"red",
"cx": circleData.xcoord,
"cy": circleData.ycoord,
"r":radius,
"opacity": 0.4,
"fill-opacity":0.1
})
.transition()
.duration(function(){
return circleData.alarmLevel*100;
})
.attr("r", radius + 100 )
.attr("opacity", 0)
.remove();
transition.each('end', myTransition);
}
datapoints.each(myTransition);
}
This is the latest code..
makeRings()
var t = window.setInterval(makeRings, 10000);
function makeRings() {
var datapoints = mapSVG.selectAll("circle.location");
var radius = 1;
function myTransition(circleData){
console.log("circleData", circleData);
var transition = d3.select(this).transition();
speedLineGroup.append("circle")
.attr({"class": "ring",
"fill":"red",
"stroke":"red",
"cx": circleData.x * ratio,
"cy": circleData.y * ratio,
"r":radius,
"opacity": 0.4,
"fill-opacity":0.1
})
.transition()
.duration(function(){
return (circleData.redSum * 100);
})
.attr("r", radius + 30 )
.attr("opacity", 0)
.remove();
transition.each('end', myTransition);
}
datapoints.each(myTransition);
}

The example you linked to uses minified code, so it's a bit of a pain to figure out what they're doing. However, if you just watch the changes in the DOM inspector, you'll see that each ring is a new circle that gets added, grows in size and faces away, and then gets removed. The different points vary in how big the rings get before they fade away (and therefore in how many rings are visible at a time, since they all grow at the same speed).
The approach I would take to make this continue indefinitely is:
Use 'setInterval' to call a function on a regular basis (e.g., once or twice per second) that will create a new ring around each data circle.
Create the rings using an .each() call on your data circles, but add them to a different <g> element, and/or with different class names so there is no confusion between the rings and the data points.
Set the initial radius of the ring to be the same as the data point, but then immediately start a transition on it. Make the duration of the transition a function of the "intensity" data value for the associated data circle, and also make the final radius a function of that data value. Also transition the opacity to a value of 0.
Make the final line of the transition for the rings .remove() so that each ring removes itself after it has finished expanding.
Basic code:
window.setInterval(makeRings, 1000);
function makeRings() {
datapoints.each(function(circleData){
//datapoints is your d3 selection of circle elements
speedLineGroup.append("circle")
.attr({"class": "ring",
"fill":"red", //or use CSS to set fill and stroke styles
"stroke":"red",
"cx": circleData.xCoord,
"cy": circleData.yCoord,
//position according to this circle's position
"r":radius, //starting radius,
//set according to the radius used for data points
"opacity": 0.8, //starting opacity
"fill-opacity":0.5 //fill will always be half of the overall opacity
})
.transition()
.duration( intensityTimeScale(circleData.intensity) )
//Use an appropriate linear scale to set the time it takes for
//the circles to expand to their maximum radius.
//Note that you *don't* use function(d){}, since we're using the data
//passed to the .each function from the data point, not data
//attached to the ring
.attr("r", radius + intensityRadiusScale(circleData.intensity) )
//transition radius
//again, create an appropriate linear scale
.attr("opacity", 0) //transition opacity
.remove(); //remove when transition is complete
});
}
Because both the change in radius and the duration of the transition are linear functions of the intensity value, the change will have a constant speed for all the data points.

All you need to do to create looping transitions in d3 is to use the end callback on transitions. Create two functions, which each create a transition on your data, with one going from your start point to your end point, and the other going back, and have them call each other on completion, like so:
function myTransition(d){
var transition = d3.select(this).transition();
//Forward transition behavior goes here
//Probably create a new circle, expand all circles, fade out last circle
transition.each('end', myTransition); //This calls the backward transition
}
d3.select('myFlashingElement').each(myTransition);
This will encapsulate everything and keep looping at whatever the duration of your transition is. The next transition will always fire when the transition before it ends, so you don't have to worry about syncing anything.

Related

How can I make the D3 Collide force apply only on X

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

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>

D3js map marker collision detection

Is there an example floating around of collision detection that avoids collision by manipulating radius rather than x,y coordinates? I'm aware of the examples Mike Bostock and others have put together, but I'm not using a force graph and my points are geographic and can't have their coordinates manipulated.
My best-guess implementation would be to begin with circles of radius 0, iterate over them and increase their individual radii as long as they don't collide with another circle. I think this would make a fantastic visualization, but I'm not sure how to efficiently determine whether one circle collides with another.
JSBin of my map with inline D3js (JavaScript tab is simply holding a 600kb GeoJSON dataset): http://jsbin.com/tapuhefamu/1/edit?html,output
Notice how the markers overlap when zoomed, it doesn't seem like a big deal in the fiddle (just zoom in further, right?) but the map I'm working with has ~2,000 pins clustered in only a few counties which need to display an informative DIV when clicked. Some pins are almost completely obscured and aren't able to be interacted with because of the overlap.
I have coded up something for you. Detecting the collision is pretty easy, basically calculate the distance between the two center points, and if the distance is less than the two radii added together, then they must have collided.
I had some issues with jsbin, so I've turned it into a gist, which you can view at http://bl.ocks.org/benlyall/6a81499abf7a0e2ad304
The interesting bits are:
Add a radiusStep parameter - use this to balance the trade off between the number of iterations, and the amount of potential overlap between nodes.
radiusStep = 0.01,
Remove the radius scaling from the zoom handler:
zoom = d3.behavior.zoom().on("zoom",function() {
g.attr("transform","translate("+ d3.event.translate.join(",")+")scale("+d3.event.scale+")");
//g.selectAll("circle")
//.attr("r", nodeRadius / d3.event.scale);
g.selectAll("path")
.style('stroke-width', countyBorderWidth / d3.event.scale )
.attr("d", path.projection(projection));
}),
Create a new structure to keep track of whether a node has collided with another, the radius and also the x and y position (pre calculated with your projection)
nodes = nodeGeoData.map(function(n) {
var pos = projection(n);
return {
collided: false,
x: pos[0],
y: pos[1],
r: 0
};
});
Two new functions to work with detecting the collision and expanding the radius until the collision is detected.
function tick() {
nodes.forEach(collided);
nodes.forEach(function(n) {
if (!n.collided) {
n.r += radiusStep;
if (n.r > nodeRadius) {
n.r = nodeRadius;
n.collided = true;
}
}
});
}
This tick function first calls collide on each node to determine if it has collided with any other. It then increases the radius by radiusStep of any node that has not collided. If the radius becomes larger than the nodeRadius parameter, then it sets the radius to that value and marks it as collided to stop it being increased.
function collided(node, i) {
if (node.collided) return;
nodes.forEach(function(n, j) {
if (n !== node) {
var dx = node.x - n.x, dy = node.y - n.y,
l = Math.sqrt(dx*dx+dy*dy);
if (l < node.r + n.r) {
node.collided = true;
n.collided = true;
}
}
});
}
The collided function checks each node to see if has collided with any other (except itself, for obvious reasons). If it detects a collision then both nodes in the comparison are marked as collided. To detect the actual collision the differences in the x and y position are calculated and then using Pythagoras the distance between them is calculated. If that distance is less than the radii of the two nodes added together, then a collision occurs.
The drawMap function is updated to calculate the radii before drawing the nodes.
while (nodes.filter(function(n) { return n.collided; }).length < nodes.length) {
tick();
}
This will basically just call the tick function until all nodes are marked as collided.
The drawNodes function is updated to use the new nodes data structure:
function drawNodes(nodes) {
g.selectAll('circle').data(nodes).enter().append("circle")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", function(d, i) { return d.r; })
.attr("class", "map-marker");
}
The changes here just reference the x, y and r attributes of each node object created earlier.
Though this works, and seems to be pretty effective, it is naive and will quickly get bogged down, since the combination of the tick and collided functions is O(n^2).

How to update both the content and location of text labels on a D3 pie chart

I'm trying to make a pie chart with d3.js that looks like this:
Note that the labels are placed along the edges of the pie chart. Initially, I am able to draw the pie charts and properly place the text nodes (the fiddle only displays one pie chart; assume, however, that they all have data that works and is appropriate, as this one does). However, when I go to adjust the data, I can't seem to .attr(translate, transform) them to the correct region along the edge of the pie chart (or do anything to them, for that matter):
changeFunctions[i] = function (data, i) {
path.data(pie(data))
.transition()
.duration(750)
.attrTween("d", arcTween);
text.each(function (d, num) {
d3.select(this)
.text(function (t) {
return t.data.name+". "+(100 * t.data.votes/totalVotes).toFixed(0) + "%";
})
/*
.attr("transform", function (d) {
//console.log("the d", d)
var c = arc.centroid(d),
x = c[0], y = c[1],
h = Math.sqrt(x * x + y * y);
return "translate(" + (x/h * 100) + ',' + (y/h * 100) + ")";
})*/
.attr("opacity", function (t) {
return t.data.votes == 0 ? 0 : 1;
});
})
}
I have omitted the general code to draw the pie chart; it's in the jsfiddle. Basically, I draw each of the pie charts in a for loop and store this function, changeFunctions[i], in a closure, so that I have access to variables like path and text.
The path.data part of this function works; the pie chart properly adjusts its wedges. The text.each part, however, does not.
How should I go about making the text nodes update both their values and locations?
fiddle
When updating the text elements, you also need to update the data that's bound to them, else nothing will happen. When you create the elements, you're binding the data to the g element that contains the arc segment and text. By then appending path and text, the data is "inherited" to those elements. You're exploiting this fact by referencing d when setting attributes for those elements.
Probably the best way to make it work is to use the same pattern on update. That is, instead of updating only the data bound to the path elements as you're doing at the moment, update the data for the g elements. Then you can .select() the descendant path and text elements, which will again inherit the new data to them. Then you can set the attributes in the usual manner.
This requires a few changes to your code. In particular, there should be a variable for the g element selection and not just for the paths to make things easier:
var g = svg.selectAll("g.arc")
.data(pie(data));
g.enter()
.append("g").attr("class", "arc");
var path = g.append("path");
The changeFunction code changes as follows:
var gnew = g.data(pie(data));
gnew.select("path")
.transition()
.duration(750)
.attrTween("d", arcTween);
Now, to update the text, you just need to select it and reset the attributes:
gnew.select("text")
.attr("transform", function(d) { ... });
Complete demo here.

Setting the circle radius in d3js with a dynamic variable

I have a set of data in searchButton function that calls different numbers in an array like so: values[80,20,10] Then in my x variable I have this scale for d3js.
x = d3.scale.linear()
.domain([0, d3.max(values)])
.range([0, width]);
Now I have a function called grow and within it I grow a set of circles based on the values in searchbutton function.
this.grow = grow;
function grow(size) {
circle.attr("r",size / 5 + 10).transition().duration(2000);
}
when I set the grow function with size as the parameter it grows but transition is not working at all on it.
In order for a d3 transition to work, you need to animate a property starting from a default value to the desired value. What would I do is following:
this.grow = grow;
function grow(size) {
circle
.attr('r', 0)
.transition()
.ease('linear')
//.ease('cubic-in-out') // default
.duration(2000)
.attr('r', size / 5 + 10);
}
This will animate a circle from 0 radius to the desired radius. You may need to adjust your logic, maybe you don't want as default radius (before transition) a value of 0.
Could you make a fiddle or post your entire code? How are you calling the grow function?

Categories

Resources