Flush a d3 v4 transition - d3.js

Does someone know of a way to 'flush' a transition.
I have a transition defined as follows:
this.paths.attr('transform', null)
.transition()
.duration(this.duration)
.ease(d3.easeLinear)
.attr('transform', 'translate(' + this.xScale(translationX) + ', 0)')
I am aware I can do
this.paths.interrupt();
to stop the transition, but that doesn't finish my animation. I would like to be able to 'flush' the transition which would immediately finish the animation.

If I understand correctly (and I might not) there is no out of the box solution for this without going under the hood a bit. However, I believe you could build the functionality in a relatively straightforward manner if selection.interrupt() is of the form you are looking for.
To do so, you'll want to create a new method for d3 selections that access the transition data (located at: selection.node().__transition). The transition data includes the data on the tweens, the timer, and other transition details, but the most simple solution would be to set the duration to zero which will force the transition to end and place it in its end state:
The __transition data variable can have empty slots (of a variable number), which can cause grief in firefox (as far as I'm aware, when using forEach loops), so I've used a keys approach to get the non-empty slot that contains the transition.
d3.selection.prototype.finish = function() {
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].duration = 0;
})
}
If working with delays, you can also trigger the timer callback with something like: if(slots[d]) slots[d].timer._call();, as setting the delay to zero does not affect the transition.
Using this code block you call selection.finish() which will force the transition to its end state, click a circle to invoke the method:
d3.selection.prototype.finish = function() {
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].timer._call();
})
}
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var circle = svg.selectAll("circle")
.data([1,2,3,4,5,6,7,8])
.enter()
.append("circle")
.attr("cx",50)
.attr("cy",function(d) { return d * 50 })
.attr("r",20)
.on("click", function() { d3.select(this).finish() })
circle
.transition()
.delay(function(d) { return d * 500; })
.duration(function(d) { return d* 5000; })
.attr("cx", 460)
.on("end", function() {
d3.select(this).attr("fill","steelblue"); // to visualize end event
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.12.0/d3.min.js"></script>
Of course, if you wanted to keep the method d3-ish, return the selection so you can chain additional methods on after. And for completeness, you'll want to ensure that there is a transition to finish. With these additions, the new method might look something like:
d3.selection.prototype.finish = function() {
// check if there is a transition to finish:
if (this.node().__transition) {
// if there is transition data in any slot in the transition array, call the timer callback:
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].timer._call();
})
}
// return the selection:
return this;
}
Here's a bl.ock of this more complete implementation.
The above is for version 4 and 5 of D3. To replicate this in version 3 is a little more difficult as timers and transitions were reworked a bit for version 4. In version three they are a bit less friendly, but the behavior can be achieved with slight modification. For completeness, here's a block of a d3v3 example.

Andrew's answer is a great one. However, just for the sake of curiosity, I believe it can be done without extending prototypes, using .on("interrupt" as the listener.
Here I'm shamelessly copying Andrew code for the transitions and this answer for getting the target attribute.
selection.on("click", function() {
d3.select(this).interrupt()
})
transition.on("interrupt", function() {
var elem = this;
var targetValue = d3.active(this)
.attrTween("cx")
.call(this)(1);
d3.select(this).attr("cx", targetValue)
})
Here is the demo:
var svg = d3.select("svg")
var circle = svg.selectAll("circle")
.data([1, 2, 3, 4, 5, 6, 7, 8])
.enter()
.append("circle")
.attr("cx", 50)
.attr("cy", function(d) {
return d * 50
})
.attr("r", 20)
.on("click", function() {
d3.select(this).interrupt()
})
circle
.transition()
.delay(function(d) {
return d * 500;
})
.duration(function(d) {
return d * 5000;
})
.attr("cx", 460)
.on("interrupt", function() {
var elem = this;
var targetValue = d3.active(this)
.attrTween("cx")
.call(this)(1);
d3.select(this).attr("cx", targetValue)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="500"></svg>
PS: Unlike Andrew's answer, since I'm using d3.active(node) here, the click only works if the transition had started already.

Related

Using RequireJS to load D3 and Word Cloud Layout

I am experiencing issues when trying to load D3 v4.8 and word cloud layout component (https://github.com/jasondavies/d3-cloud) using the RequireJS. While both D3.js and d3.layout.cloud.js are being downloaded to the browser, an exception is being thrown indicating the d3.layout.cloud is not a function.
Here is how I am configuring the RequireJS:
require.config({
waitSeconds: 10,
baseUrl: './scripts',
paths: {
d3: 'd3.min',
jquery: 'jquery-2.1.0.min',
cloud: 'd3.layout.cloud'
},
shim: {
cloud: {
deps:['jquery', 'd3']
}
}
});
The line of code that throws an exception is d3.layout.cloud().size([width, height]) and can be found in the function below:
function wordCloud(selector) {
var width = $(selector).width();
var height = $(selector).height();
//var fill = d3.scale.category20();
//Construct the word cloud's SVG element
var svg = d3.select(selector).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate("+ width/2 +","+ height/2 +")")
var fill = d3.scale.category20();
//Draw the word cloud
function draw(words) {
var cloud = svg.selectAll("g text")
.data(words, function(d) { return d.text; })
//Entering words
cloud.enter()
.append("text")
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr('font-size', 1)
.style("cursor", "hand")
.text(function(d) { return d.text; })
.on("click", function (d, i){
window.open("http://www.google.com/?q=" + d.text, "_blank");
});
//Entering and existing words
cloud
.transition()
.duration(600)
.style("font-size", function(d) { return d.size + "px"; })
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.style("fill-opacity", 1);
//Exiting words
cloud.exit()
.transition()
.duration(200)
.style('fill-opacity', 1e-6)
.attr('font-size', 1)
.remove();
}
//Use the module pattern to encapsulate the visualisation code. We'll
// expose only the parts that need to be public.
return {
//Recompute the word cloud for a new set of words. This method will
// asycnhronously call draw when the layout has been computed.
//The outside world will need to call this function, so make it part
// of the wordCloud return value.
update: function(words) {
// min/max word size
var minSize = d3.min(words, function(d) { return d.size; });
var maxSize = d3.max(words, function(d) { return d.size; });
var textScale = d3.scale.linear()
.domain([minSize,maxSize])
.range([15,30]);
d3.layout.cloud().size([width, height])
.words(words.map(function(d) {
return {text: d.text, size: textScale(d.size) };
}))
.padding(5)
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return d.size; })
.on("end", draw)
.start();
}
}
}
The latest version of d3-cloud only depends on d3-dispatch so it should work fine with any version of D3. I think the issue here is that you're using RequireJS (AMD) to reference the d3.layout.cloud.js file, but you're not using RequireJS to use the library (configured as cloud in your example). See the following example:
requirejs.config({
baseUrl: ".",
paths: {
cloud: "d3.layout.cloud" // refers to ./d3.layout.cloud.js
}
});
requirejs(["cloud"], function(cloud) { // depends on "cloud" defined above
cloud()
.size([100, 100])
.words([{text: "test"}])
.on("word", function() {
console.log("it worked!");
})
.start();
});
If you prefer to use CommonJS-style require(…) you can also use this with RequireJS if you use the appropriate define(…) syntax, like in this quick example.
While d3-cloud itself is compatible with both D3 V3 and V4, most examples you will find are not, and your current code is not, it can only work with V3.
To make it work with V4 you need to replace all references to d3.scale.
For example, d3.scale.category20() becomes d3.scaleOrdinal(d3.schemeCategory20).
For a small runnable example that is compatible with both, see Fiddle #1. It uses RequireJS to load d3, d3-cloud, and jQuery. Try changing the D3 version in require config at top of JS part.
Let's settle on V3 for now, since your code relies on it. There are still a few issues:
Your must use objects for d3 & d3cloud & jQuery that are obtained via a call to require. And with RequireJS this is asynchronous (because it needs to fetch the JS scripts programmatically, and in a browser that can only be done asynchronously). You put your code within a callback function (for more info, see RequireJS docs):
[ Edit: see Jason's answer for alternative syntax. ]
require(['d3', 'd3cloud', 'jQuery'], function(d3, d3cloud, $) {
/* Your code here.
Inside "require" function that you just called,
RequireJS will fetch the 3 named dependencies,
and at the end will invoke this callback function,
passing the 3 dependencies as arguments in required order.
*/
});
In this context (and versions) d3 cloud main function is not available under d3.layout.cloud(), so you need to replace that with d3cloud(), assuming that's the name of the argument passed as callback to require.
You must make sure you never pass width or height of 0 to d3cloud.size([width, height]), or it will enter an infinite loop. Unfortunately it can easily happen if you use $(selector).height(), depending on content of your page, and possible "accidents". I suggest var height = $(selector).height() || 10; for example.
Not a programming problem, but the function you pass to .rotate() is taken from an example, and maybe you want to change that: it yields only 2 possible values, 0 or 90, I find this monotonous, the default one is prettier. So I removed the line entirely in the example below. Maybe you'll want it back, just add your .rotate(...) line.
Fiddle #2: a complete working example based on your piece of code.

d3 v4 forces acting on dynamically added nodes

I'm trying to add nodes one by one to a d3 force simulation (in version 4!) but some do not seem to be being evolved by the simulation after they've been created.
Currently the simulation assigns one node, then a function, addNode is called twice adding two more nodes. Each is added to the simulation, has a circle and a line rendered and has a cursor event added, one by one.
(Technically the first and second node are done at the same time, as the first is only set up when addNode is called on the second)
Then, when a node is clicked, a new node, connected to the one under the cursor, should be created. This node should then evolve under the forces of the simulation like any other.
However, whilst one or two nodes seem to be created fine, later nodes don't seem to be evolving under the simulation. Specifically the many-body force, which should be keeping some space between nodes, doesn't seem to function.
My intuition is that the nodes are being added at an inopportune time for the simulation's ticked function (earlier problems were solved by adding some simulation.stop and simulation.restart commands any time new nodes were being added) but in theory the simulation should be paused whenever new bodies are being added.
Is this a correct implementation of dynamically adding nodes in d3 v4, or are the issues with forces just highlighting a mangled method? This previous answer helped me to realize that I needed to merge new entries, but forces seem to be working fine there.
var w = 250;
var h = 250;
var svg = d3.select("body").append("svg");
svg.attr('width', w)
.attr('height', h);
// ensures links sit beneath nodes
svg.append("g").attr("id", "lnks")
svg.append("g").attr("id", "nds")
function new_node(id) {
this.id = id;
this.x = w / 2;
this.y = h / 2;
}
function new_link(source, target) {
this.source = source;
this.target = target;
}
var nodes = [];
var links = [];
var node;
var circles;
var link;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(80).id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody().strength(-1000))
.force("xPos", d3.forceX(w / 2))
.force("yPos", d3.forceY(h / 2))
.on('tick', ticked);
simulation.stop();
var newNode = new new_node(0);
nodes.push(newNode);
for (var i = 1; i < 3; i++) {
if (i == 3) continue;
addNode(0, i)
}
function addNode(rootId, newId) {
var newNode = new new_node(newId);
nodes.push(newNode);
var newLink = new new_link(rootId, newId);
links.push(newLink);
//adds newest link and draws it
link = svg.select("#lnks").selectAll(".link")
.data(links)
var linkEnter = link
.enter().append("line")
.attr("class", "link");
link = linkEnter.merge(link);
//adds newest node
node = svg.select("#nds").selectAll(".node")
.data(nodes)
var nodeEnter = node
.enter().append("g")
.attr("class", "node");
//draws circle on newest node
var circlesEnter = nodeEnter.append('circle')
node = nodeEnter.merge(node);
circles = d3.selectAll('circle');
simulation.stop();
simulation.nodes(nodes);
simulation.force("link")
.links(links);
restartSim();
}
//starts up the simulation and sets up the way the leaves react to interaction
function restartSim() {
simulation.restart();
circles.on('click', function(d, i) {
addNode(i, nodes.length)
})
}
function ticked() {
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
.link {
stroke: #bbb;
}
.node circle {
pointer-events: all;
fill: black;
stroke-width: 0px;
r: 20px
}
h1 {
color: white;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Code also on codepen here:
http://codepen.io/zpenoyre/pen/kkxBRW?editors=0010
The force simulations as the name implies is a simulation on particles interacting with each other. Alpha is used to help converge the system by decaying at each iteration. The forces are multiplied by alpha, so at each iteration the forces become weaker until alpha reaches a very low value when the simulation stops. From d3 documentation:
simulation.restart() <>
Restarts the simulation’s internal timer and returns the simulation.
In conjunction with simulation.alphaTarget or simulation.alpha, this
method can be used to “reheat” the simulation during interaction, such
as when dragging a node, or to resume the simulation after temporarily
pausing it with simulation.stop.
When you add a node, the simulation already stopped, so, you need to "reheat" the simulation with alpha 1.
simulation.force("link")
.links(links);
simulation.alpha(1); // <---- reheat;
restartSim();
Here is the updated code pen:
http://codepen.io/anon/pen/amqrWq?editors=0010

Update D3 chart dynamically

The idea is to have a d3 vertical bar-chart that will be given live data.
I simulate the live data with a setInterval function that updates the the values of the elements in my dataset:
var updateData = function(){
a = parseInt(Math.random() * 100),
b = parseInt(Math.random() * 100),
c = parseInt(Math.random() * 100),
d = parseInt(Math.random() * 100);
dataset = [a, b, c, d];
console.log(dataset);
};
// simullate live data input
var update = setInterval(updateData, 1000);
I want to update the chart every 2 seconds.
For that I need a update function that gets the new dataset and then animates a transition to show the new results.
Like that:
var updateVis = function(){
..........
};
var updateLoop = setInterval(drawVis,2000);
I don't want to simply remove the chart and draw again. I want to animate the transition between the new and old bar height for each bar.
Checkout the fiddle
Since your not changing the number of bars, this can be as simple as:
var updateVis = function(){
svg.selectAll(".input")
.data(dataset)
.transition()
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
};
Updated fiddle.
But your next question becomes, what if I need a different number of bars? This is where you need to handle enter, update, exit a little better. You you can write one function for initial draw or updating.
function drawVis(){
// update selection
var uSel = svg.selectAll(".input")
.data(dataset);
// those exiting
uSel.exit().remove();
// new bars
uSel
.enter()
.append("rect")
.attr("class", "input")
.attr("fill", "rgb(250, 128, 114)");
// update all
uSel
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5/100 * w;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", y(0))
.transition().duration(750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
}
New fiddle.
That's the way to go.
Just think what you've done to get the initial chart:
1) Get data
2) Bind it to element (.enter())
3) Set element attributes to be function of the data.
Well, you do this again:
In the function updateData you get a new dataset that's the first step.
Then, rebind it:
d3.selectAll("rect").data(dataset);
And finally update the attributes:
d3.selectAll("rect").attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
(Want transitions? Go for it. It is easy to add in your code but you better read this tuto if you want to deeply understand it)
Check it on fiddle

d3js not removing previous element when select a new JSON file

I have a problem with my first attempt at an Exit event. The code below draws one of three circles based on three different JSON files. The default is the medium sized circle. The drop-down menu allows you to select either a Small, Medium, or Large circle to be drawn next.
My problem is that I have not written the Exit mode correctly. If I comment out the exit() then each selection appears on the screen without deleting any of the previous elements, as you would expect. If the exit mode is not commented out then only the default (medium) circle is displayed.
This is likely very obvious to someone with D3JS experience. Please help me to learn where I am going wrong. Thanks so much!
- Tim
<body>
<select id = "opts">
<option value="circle1">Small</option>
<option value="circle2" selected="selected">Medium</option>
<option value="circle3">Large</option>
</select>
<script>
// data sources for small, med, large circles
var circle1 = "/MenuSelections/circle1.JSON";
var circle2 = "/MenuSelections/circle2.JSON";
var circle3 = "/MenuSelections/circle3.JSON";
function drawCirc(newData){
// Read in the JSON data
console.log(newData); // Confirms data source
d3.json(newData, function(dataset) {
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
//ENTER
var circles = svgContainer.selectAll("circle")
.data(dataset.circle)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) {
console.log(d)
return d.x_axis; })
.attr("cy", function (d) { return d.y_axis; })
.attr("r", function (d) { return d.radius; })
.style("fill", function(d) { return d.color; });
//EXIT - NOT WORKING. Original remains, no new graph.
// if this section commented out: new circles draw in without remove of prev.
d3.select("body").selectAll("circle")
.data(dataset.circle)
.exit()
.remove();
});
}
drawCirc(circle2); // Initiate with default selection
// handle on click event for the ID (opts) of the menu item
d3.select('#opts')
.on('change', function() {
var newData = eval(d3.select(this).property('value'));
// console.log(newData);
drawCirc(newData); // Call with new selection.
});
The advice from both Lars and Ben got me on the right track, with two key parts to the solution. The first was understanding that exit() acts on the elements, not their data values, and I needed to add a key as suggested above. Once I fixed that I still had a problem because I was defining my svg container WITHIN the drawCirc function, so each new selection resulted in a new SVG 200x200 container.
Good information on Enter, Update, Exit can be found here
http://bost.ocks.org/mike/circles/ and here http://bl.ocks.org/mbostock/3808218
The book "Data Visualization with D3 and AngularJS" Chapter 1, pages 13-16 is also very helpful.
Here is my revised code.
<body>
<select id = "opts">
<option value="circle1">Small</option>
<option value="circle2" selected="selected">Medium</option>
<option value="circle3">Large</option>
</select>
<script>
// var names correspond to the value of each drop down item
var circle1 = "/MenuSelections/circle1.JSON";
var circle2 = "/MenuSelections/circle2.JSON";
var circle3 = "/MenuSelections/circle3.JSON";
var svg = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
function drawCirc(newData){
console.log(newData); // Confirms data source
// Read in the JSON data
d3.json(newData, function(dataset) {
//ENTER
var circle = svg.selectAll("circle")
.data(dataset.circle, function(d) {return d;});
circle.enter().append("circle");
circle.attr("cx", function (d) {
console.log(d)
return d.x_axis; })
.attr("cy", function (d) { return d.y_axis; })
.attr("r", function (d) { return d.radius; })
.style("fill", function(d) { return d.color; });
//EXIT
circle.exit().remove();
});
}
drawCirc(circle2); // Initiate with default selection
// handle on click event for the ID (opts) of the menu item
d3.select('#opts')
.on('change', function() {
var newData = eval(d3.select(this).property('value'));
drawCirc(newData); // Call with new selection.
});
</script>
</body>

Dragging a group of elements and zooming in D3

I was able to incorporate the Pan/Zoom example fine. The problem is I also want to be able to drag various groups of elements in addition to zooming. What I'm doing now, is disabling the "pan" part of the zoom/drag behavior, and having a separate drag event listener on the groups I want the user to be able to drag. The problem now is that when I drag a group, then go to zoom in, the group jumps. I was wondering what the best way to go about fixing this problem might be? I was thinking about changing all of the individual elements positions' directly instead of just translating the whole group, but that seems really brittle and inefficient to me.
EDIT: Here is my code. I've commented the pertinent parts, but left the whole directive for context
window.angular.module('ngff.directives.board', [])
.directive('board', ['socket',
function(socket) {
var linker = function(scope, elem, attrs) {
var nodeDragging = false;
scope.svg = d3.select(elem[0]).append("svg")
.attr('width', elem.parent().width())
.attr('height', elem.parent().height())
.attr('class', 'boardBackground')
.on('mouseup', function(){
nodeDragging = false;
})
.append('g')
.call(d3.behavior.zoom().on('zoom', zoom))
.append('g')
scope.svg.append("rect")
.attr("class", "overlay")
.attr("width", elem.parent().width())
.attr("height", elem.parent().height());
scope.$on('addFormation', function(event, formation) {
var group = formation.select('g');
scope.svg.node().appendChild(group.node())
var pieces = group.selectAll('circle')
.on('mousedown', function(){
//Here I set a flag so I can check for node dragging when my zoom function is called
nodeDragging = true;
})
.call(d3.behavior.drag().on('drag', move));
})
function move() {
var dragTarget = d3.select(this);
dragTarget
.attr('cx', function() {
return d3.event.dx + parseInt(dragTarget.attr('cx'))
})
.attr('cy', function() {
return d3.event.dy + parseInt(dragTarget.attr('cy'))
})
}
//******I return here if the user is dragging the node to keep all of the elements from being translated
function zoom() {
if(nodeDragging)return
console.log('zoom')
scope.svg
.attr("transform","translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
}
}
return {
restrict: 'E',
link: linker
}
}
])

Resources