Dragging a group of elements and zooming in D3 - d3.js

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

Related

d3: how to drag line elements independently of background

I have developed an applet that shows a d3 diagonal tree. The graph is navigatable by dragging the background.
It is based on the code found at the following link:
https://bl.ocks.org/adamfeuer/042bfa0dde0059e2b288
I am trying to have vertical lines across the page to further annotate the tree/ graph (based on the following link: https://bl.ocks.org/dimitardanailov/99950eee511375b97de749b597147d19).
See below:
See here: https://jsfiddle.net/chrisclarkson100/opfq6ve8/28/
I append the lines to the graph as follows:
var data_line = [
{
'x1': 300,
'y1': 700,
'x2': 300,
'y2': 700
},
////....
];
// Generating the svg lines attributes
var lineAttributes = {
....
'x1': function(d) {
return d.x1;
},
'y1': function(d) {
return screen.availHeight;
},
'x2': function(d) {
return d.x2;
},
'y2': function(d) {
return 0;
}
};
var drag_line = d3.behavior.drag()
.origin(function(d) { return d; })
.on('drag', dragged_line);
// Pointer to the d3 lines
var svg = d3.select('body').select('svg');
var lines = svg
.selectAll('line')
.data(data_line)
.enter().append('g')
.attr('class', 'link');
links_lines=lines.append('line')
.attr(lineAttributes)
.call(drag_line);
lines.append('text')
.attr('class','link_text')
.attr("x", d => d.x1)
.attr("y", d => 350)
.style('fill', 'darkOrange')
.style("font-size", "30px")
function dragged_line() {
var x = d3.event.dx;
var y = d3.event.dy;
var line = d3.select(this);
// Update the line properties
var attributes = {
x1: parseInt(line.attr('x1')) + x,
y1: parseInt(line.attr('y1')) + y,
x2: parseInt(line.attr('x2')) + x,
y2: parseInt(line.attr('y2')) + y,
};
line.attr(attributes);
}
The lines display as I wanted and are draggable. However, when I drag them, the background/ tree network moves with them... I want the different draggable elements to be independent of eachother.
Can anybody spot how I'm failing to make the dragging interactivity of each element independent of eachother?
Here's a JSFiddle that seems to do what you want– you can move the vertical lines without moving the tree; and move the tree without moving the vertical lines:
https://jsfiddle.net/adamfeuer/gd4ouvez/125/
(That JSFiddle uses a much smaller dataset of tree nodes; the one you linked to was too big to easily iterate and debug.)
The issue with the code you posted is that the zoom (pan) function for the tree is active at the same time the zoom() for the lines is active, so the tree and the active line drag at the same time.
I added a simple mechanism to separate the two – a boolean called lineDragActive. The code then checks for that in the tree zoom(), sets it true when a line drag starts, and false when the line drag ends:
// Define the zoom function for the zoomable tree
// flag indicates if line dragging is active...
// if so, we don't want to drag the tree
var lineDragActive = false;
function zoom() {
if (lineDragActive == false) {
// not line dragging, so we can zaoom (drag) the tree
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
[...]
function drag_linestarted() {
// tell others not to zoom while we are zooming (dragging)
lineDragActive = true;
d3.select(this).classed(activeClassName, true);
}
function drag_lineended() {
// tell others that zooming (dragging) is allowed
lineDragActive = false;
d3.select(this).classed(activeClassName, false);
label = baseSvg.selectAll('.link_text').attr("transform", "translate(" + String(Number(this.x1.baseVal.value) - 400) + "," + 0 + ")");
}

Flush a d3 v4 transition

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.

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.

Replace data with new set

Using D3.js, I have something like this:
var sets = [
{ data:[{date:1980,value:10},{date:1981,value:20},{date:1982,value:30}] },
{ data:[{date:1981,value:10},{date:1982,value:20},{date:1983,value:30}] },
{ data:[{date:1982,value:10},{date:1983,value:20},{date:1984,value:30}] }
];
And I bind it to make a chart like this:
var paths = g.selectAll("path")
.data(sets);
paths.enter()
.append("path")
.datum(function(d) { return d.data; })
.attr("class","line")
.attr("d", line);
Where g is a g element inside an svg element. This works. For each item in set I get a path using the values in data. Now what I want to do is click an element and replace the data with a different set:
var altData = [
{ data:[{date:1980,value:30},{date:1981,value:20},{date:1982,value:10}] },
{ data:[{date:1981,value:10},{date:1982,value:20},{date:1983,value:30}] },
{ data:[{date:1982,value:10},{date:1983,value:20},{date:1984,value:0}] }
];
d3.select("#transition").on("click", function() {
paths.data(altData);
console.log("click");
});
But the paths.data(altData) doesn't appear to do anything. There are no console errors, but the chart doesn't change. What do I need to do to tell it that the data has changed and the lines should be redrawn? As a bonus, I'd really like this transition to be animated.
Full fiddle
Basically you need to tell d3 to redraw it. In your case, it is by calling attr("d", line).
For transition, put transition() between two attr("d", fnc). Your onclick function will look like the following
d3.select("#transition").on("click", function() {
paths.attr("d", line)
.transition()
.attr("d", function(d, i){
return line(altData[i].data)
})
});
Jsfiddle http://jsfiddle.net/8fLufc65/
Look at this plnkr that will change the data when transition is clicked.
I made the part that draws the lines into a function and pass the data for which it should be drawing the lines.
drawPaths(sets) ;
function drawPaths(sets) {
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var paths = g.selectAll("path")
.data(sets);
paths.enter()
.append("path")
.datum(function(d) { console.log(d); return d.data; })
.attr("class","line")
.attr("d", line);
}

Redrawing the stack bar chat on click on the toggle legend using D3.js

I want to implement stack bar with toggle legend using D3.js ,on click on the legend, stack bar should get redrawn.If the legend was active,rectangle slab corresponding to the legend should get disappear and vise versa.
On click on the legend, I am not able to update the data binded with the group element and rect element present inside the group element properly.
In the DOM tree,on click on the legend,rect element is getting appended and added to first group element, rect element should actually get updated only.
You can view the source code in Jsfiddle here
I want something similar to stack bar with legend selection as implemented here in nvd3
function redraw() {
var legendselector = d3.selectAll("g.rect");
var legendData = legendselector.data();
var columnObj = legendData.filter(function(d, i) {
if (d.active == true)
return d;
});
var remapped = columnObj.map(function(cause) {
return dataArch.map(function(d, i) {
return {
x : d.timeStamp,
y : d[cause.errorType]
};
});
});
var stacked = d3.layout.stack()(remapped);
valgroup = stackBarGroup.selectAll("g.valgroup").data(stacked, function(d) {
return d;
}).attr("class", "valgroup");
valgroup.enter().append("svg:g").attr("class", "valgroup").style("fill",
function(d, i) {
return columnObj[i].color;
}).style("stroke", function(d, i) {
return d3.rgb(columnObj[i].color).darker();
});
valgroup.exit().remove();
rect = valgroup.selectAll("rectangle");
// Add a rect for each date.
rect = valgroup.selectAll("rectangle").data(function(d, i) {
return d;
}).enter().append('rect');
valgroup.exit().remove();
rect.attr("x", function(d) {
return x(d.x);
}).attr("y", function(d) {
return y(d.y0 + d.y);
}).attr("height", function(d) {
return y(d.y0) - y(d.y0 + d.y);
}).attr("width", 6);
}
function redraw() did not use transition inside it
You need to get more understanding about object constancy. (Three state described by the author)
I wrote an example of group chart in d3, the legend is interactable and works well, because i am new to d3, maybe the pattern or standard used is not very formal.
Listed it below only for you reference, hope it helps, good luck :-p
fiddle

Resources