How to use drag as a 'function' instead of a 'var'? - javascript

First off I was using this for dragging objects on screen :
var button_drag = d3.behavior.drag()
d3.behavior.drag()
.origin(function() {
var g = this;
return {x: d3.transform(g.getAttribute("transform")).translate[0],
y: d3.transform(g.getAttribute("transform")).translate[1]};
})
.on("drag", function(d) {
g = this;
translate = d3.transform(g.getAttribute("transform")).translate;
x = d3.event.dx + translate[0],
y = d3.event.dy + translate[1];
//boundaries
// if(x<10){x=10}
// if(x>100){x=100}
if(y<10){y=10}
if(y>200){y=200}
d3.select(g).attr("transform", "translate(" + x + "," + y + ")");
d3.event.sourceEvent.stopPropagation();
});
call it like so :
buttons.call(buttons_drag);
Now I want to change this to a function so I can pass variables through it to set different boundaries for different objects:
function button_drag(xLower,xhigher,yLower,yHigher){
d3.behavior.drag()
.origin(function() {
var g = this;
return {x: d3.transform(g.getAttribute("transform")).translate[0],
y: d3.transform(g.getAttribute("transform")).translate[1]};
})
.on("drag", function(d) {
g = this;
translate = d3.transform(g.getAttribute("transform")).translate;
x = d3.event.dx + translate[0],
y = d3.event.dy + translate[1];
//boundaries
if(x<xLower){x=xLower}
if(x>xHigher){x=xHigher}
if(y<yLower){y=-yLower}
if(y>yHigher){y=yHigher}
d3.select(g).attr("transform", "translate(" + x + "," + y + ")");
d3.event.sourceEvent.stopPropagation();
});
}
Then call it like so :
buttons.call(buttons_drag(xLower,xHigher,yLower,yHigher));
Once I changed it to a function the whole drag capability doesn't seem to work at all and I'm not too sure why.
Should I be doing this a different way ? Basically all I want to do is to use one drag function for all objects but pass through 'boundaries' when I call the drag function.

As it currently stand, your buttons.call() function never receives a handler of the drag behavior. Previously you gave it the return value of the d3.behavior.drag() call, but you function later on does not return that reference.
So just add a return and you'll be fine:
function button_drag(xLower,xhigher,yLower,yHigher){
return d3.behavior.drag()
// ...
}

Related

d3 tooltip bar for multi line chart on mouseover on Y Axis (code supplied)

I'm trying to implement a tooltip on mouseover for a multi line chart.
I've followed the code from this example and tried to change it so that I see the X values of the lines for a given hovered Y value, but I'm not able to get it to work.
My attempt can be found below.
In my actual implementation I'm writing in Typescript and the functions 'getTotalLength()' and 'getPointAtLength()' are saying they don't exist on property Element.
Also if you can add a text box at on the line that has the hovered Y value that'd help me a lot!
https://codesandbox.io/s/modest-minsky-hvsms?fontsize=14&hidenavigation=1&theme=dark
Thanks
So after careful review there were several errors which I have corrected.
Your paths for the data lines were not assigned the class so you need to assign the class of dataLine to them when you append them like so:
svg
.selectAll(".dataLine")
.data(nestedData)
.enter()
.append("path")
.attr("fill", "none")
.attr("class", "dataLine")
.attr("stroke", d => itemMap(d.key).color)
.attr("stroke-width", d => itemMap(d.key).lineWeight)
.attr("d", d =>
d3
.line()
.x(d => x(d.xvalue))
.y(d => y(d.yvalue))(d.values)
);
As pointed out in the comment above, stop using arrow functions if you intend to use this. Once you do that, your d3.mouse(this) starts working.
The example you followed had the paths from left to right, while yours is from top to bottom. This required several changes in terms of coordinates to get the alignment of the mouseover line and the circles with the text values near them to align properly. The correct code is as follows:
.on("mousemove", function() {
//#ts-ignore
var mouse = d3.mouse(this);
d3.select(".mouse-line").attr("d", () => {
var d = "M" + plotWidth + "," + mouse[1];
d += " " + 0 + "," + mouse[1];
return d;
});
d3.selectAll(".mouse-per-line").attr("transform", function(d, i) {
var yDepth = y.invert(mouse[1]);
var bisect = d3.bisector(d => d.depth).right;
var idy = bisect(d.values, yDepth);
var beginning = 0;
var end = lines[i].getTotalLength();
var target = null;
while (true) {
target = Math.floor((beginning + end) / 2);
var pos = lines[i].getPointAtLength(target);
if (
(target === end || target === beginning) &&
pos.y !== mouse[1]
) {
break;
}
if (pos.y > mouse[1]) {
end = target;
} else if (pos.y < mouse[1]) {
beginning = target;
} else {
break;
}
}
d3.select(this)
.select("text")
.text(x.invert(pos.x).toFixed(2));
return "translate(" + pos.x + "," + mouse[1] + ")";
});
});
Fully working codesandbox here.

Drag a Line with d3.drag-event, on a raster

since two days, I'm trying to move a line in simple 10x10 Raster, using the mouse.
I solved this without any issues, with complex svg-"Symbols" but the simplest Elements bring me to my borders…
Ok, simple line with a g-tag:
var svg = d3.select("#drawing");
const graph = svg.append("g")
.attr("id","1")
.attr("ponter-events","fill")
.call(d3.drag()
.on("start", dragGraphicStart)
.on("drag", dragGraphic)
.on("end", dragGraphicStop));
graph.html("<line class='coldbrinewater graphic' x1='10' y1='10' x2='50' y2='50' />");
Now of Course, before the first drag, this line doesn't have a Transformation, so I check this before reading a Position:
var xPos, yPos;
function dragGraphicStart() {
var trans = d3.select(this).attr("transform"); //Prüfen ob bereits ein Transform existiert
if (trans == null || trans == "" || trans == "translate(0)") { //kein Transform vorhanden
xPos = 0;
yPos = 0;
}
else { //Transform gefunden, startwerte ermitteln
trans = trans.replace(",", " "); //I don't know why, sometimes I got a comma between x and y-value
trans = trans.substring(trans.indexOf("(") + 1, trans.indexOf(")")).split(" ");
xPos = parseInt( trans[0]);
yPos = parseInt( trans[1] );
}
}
And then I try to move the line:
function dragGraphic(d, i) {
xPos += Math.round(d3.event.dx / raster) * raster;
//xPos += d3.event.dx;
yPos += Math.round(d3.event.dy / raster) * raster;
//yPos += d3.event.dy;
d3.select(this).attr("transform", "translate(" + xPos + " " + yPos + ")");
}
with the commented lines xPos += d3.event.dx (and y) it works fine but not if I like to calculate the raster before.
I have no idea why, but then I can see in the console, that translate-attribute often only has one Parameter or is empty. like this:
transform"translate(30)" or transform=""
I built a (non) working example here:
https://jsfiddle.net/Telefisch/3oecj6wd/
Thank you

Why are d, this, and the variable all undefined in my drag function?

I'm trying to do some logic based on the coordinates of a rectangle, when dragged. I want to select all circles within the rectangle.
function dragmove(d) {
var barz = document.querySelector("#visual");
var point = d3.mouse(barz),
tempP = {
x: point[0],
y: point[1]
};
d3.event.sourceEvent.stopPropagation();
d3.select(this).style({
opacity: 0.05
})
console.log(selectionBox.x); //turns out undefined
console.log(d.x); //also undefined
console.log(d3.select(this)); //undefined
vis.selectAll("circle").filter(function (d, i) {
return (d.x > d3.select(this).x && d.x < (d3.select(this).x + d3.select(this).width))
}).style({
opacity: 0.1
});
If you didn't already notice, right now I only have it checking within the x coordinates, at least until I finish fixing this. Here's the fiddle.
Whenever I try to run it, it doesn't pull any errors, but it doesn't work as intended because the reference is undefined. Is there any reason why none of the references work at all?
To reproduce this you need to first drag on the canvas to draw a rectangle, and then drag that rectangle
It seems like your origin function isn't quite right. I tried the one from this answer
var drag = d3.behavior.drag()
.origin(function(d) {
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on("dragstart", dragstarter)
.on("drag", dragmove);
Now it passes in a valid object to the dragmove function.
In the following code:
vis.selectAll("circle").filter(function (d, i) {
return (d.x > d3.select(this).x && d.x < (d3.select(this).x + d3.select(this).width))
})...
the reference to this is undefined because of how the Array.prototype.filter function works. According to the specs, we can provide our own this as the second parameter to the filter function, so:
vis.selectAll("circle").filter(function (d, i) {
return (d.x > d3.select(this).x && d.x < (d3.select(this).x + d3.select(this).width))
}, this)...
Updated your fiddle

d3js Moving SVG elements inside a transforming group

I am working on a query builder project. I am trying to build a query generator using d3.js. I am stucked in a part where I want to move certain elements inside a transforming group. This is the repo and I am stucked in this function. I want to move the operators after connecting it and update the connected lines. Can anyone help me?
var circleDrag = d3.behavior.drag()
.on('dragstart', function () {
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function () {
var parentQboxGroupId = d3.select(this).select(function () {
return this.parentNode;
});
var grandParent = parentQboxGroupId.select(function(){
return this.parentNode;
});
var drawingGroup = d3.select('#'+grandParent.attr('id'));
var currentC = d3.select(this);
dragging = true;
drawingGroup
.select('.lineInsideQbox')
.attr('x1', currentC.attr('cx'))
.attr('y1', currentC.attr('cy'))
.style('stroke','green')
.style('stroke-width','2px');
dummyLine.src = currentC.attr('id');
console.log('CIRCLE IS BEING DRAGGED' + JSON.stringify(dummyLine));
})
.on('dragend', function () {
console.log('drag circle end');
//if(!selectedCircle.id){
// dummyLine.target = selectedQbox.id;
//}
dummyLine.target = selectedCircle.id;
dragging = false;
console.log('DRAG END : SELCTED NODE : '+ JSON.stringify(selectedCircle));
console.log('DRAG END : DUMMY LINE : '+ JSON.stringify(dummyLine));
var targetNode = d3.select('#'+dummyLine.target);
var srcNode = d3.select('#'+dummyLine.src);
console.log('split : ' + dummyLine.src.split('--'));
var group = '#' + (dummyLine.src).split('--')[1];
console.log('G: ' + group);
d3.select(group).append('line')
.attr('id', function () {
var a = (dummyLine.src).split('--');
var b = (dummyLine.target).split('--');
if( a[0]== 'nodeRight'){
return dummyLine.src + '__' + dummyLine.target;
}else{
return dummyLine.target + '__' + dummyLine.src;
}
})
.attr('class', function () {
var a = (dummyLine.src).split('--');
var b = (dummyLine.target).split('--');
return 'line '+ a[1]+' '+b[1];
})
.attr('x1', srcNode.attr('cx'))
.attr('y1',srcNode.attr('cy'))
.attr('x2',targetNode.attr('cx'))
.attr('y2',targetNode.attr('cy'))
.style('stroke', 'black')
.style('stroke-width', '3px')
;
dummyLine.src = null;
dummyLine.target = null;
});
EDIT : When I try to drop a query Box. I can drop other operators inside it. Then I should be able to connect them inside. Here is the image showing what I am trying.
After the connections made, I try yo move large box & small operators individually. That's where the code break.
The main issue is that to move the operator, you use a translate to move the whole group ( tag) which includes the image, the two circles and the line. You then set the other end of the line using CX, CY values of the other operator it is connected to.
This wont work because the CX and CY values of the circles are not updated when you perform a translate, so on a second move, it would put the x, y values at the original point of the circles, not to the moved point.
To resolve, instead of translating the whole group, translate only the image, update the cx and cy values of the circles and then update the line x, y values with the new cx, cy of the circles:
All of the amendments needed are within your operatorDrag.js file.
First of all, when you append the circles, add an attribute which holds the original cx and cy values. We will need these when calculating the new cx, cy when dragging the operator:
change from this:
var op = currGroup
.append('image')
.attr('class', 'operator')
.attr('width', elem.attr('width') * 0.75)
.attr('height', elem.attr('height') * 0.75)
.attr('x', d3.mouse(this)[0])
.attr('y', d3.mouse(this)[1])
.attr('xlink:href', elem.attr('href'));
currGroup
.append('circle')
.attr('class', 'node nodeLeft')
.attr('id', function () {
return 'nodeLeft--' + currGroup.attr('id');
})
.attr('cx', op.attr('x'))
.attr('cy', op.attr('height') / 2 + Number(op.attr('y')))
.attr('r', 5)
.style('fill', 'red')
.on('mouseover', function () {
selectedCircle = {
id: d3.select(this).attr('id'),
cls: 'nodeLeft'
}
})
.call(circleDrag)
;
currGroup
.append('circle')
.attr('class', 'node nodeRight')
.attr('id', function () {
return 'nodeRight--' + currGroup.attr('id');
})
.attr('cx', Number(op.attr('x')) + Number(op.attr('width')))
.attr('cy', op.attr('height') / 2 + Number(op.attr('y')))
.attr('r', 5)
.style('fill', 'red')
.on('mouseover', function () {
selectedCircle = {
id: d3.select(this).attr('id'),
cls: 'nodeRight'
}
})
.call(circleDrag)
;
To this (the updated code is contained in comments starting with #SB):
var op = currGroup
.append('image')
.attr('class', 'operator')
.attr('width', elem.attr('width') * 0.75)
.attr('height', elem.attr('height') * 0.75)
.attr('x', d3.mouse(this)[0])
.attr('y', d3.mouse(this)[1])
.attr('xlink:href', elem.attr('href'));
currGroup
.append('circle')
.attr('class', 'node nodeLeft')
.attr('id', function () {
return 'nodeLeft--' + currGroup.attr('id');
})
.attr('cx', op.attr('x'))
.attr('cy', op.attr('height') / 2 + Number(op.attr('y')))
// #SB: add a reference to the original cx and cy position.
// we will need it to set new cx cy when moving operator
.attr('data-cx', op.attr('x'))
.attr('data-cy', op.attr('height') / 2 + Number(op.attr('y')))
//----------------------------------------------------------------------
.attr('r', 5)
.style('fill', 'red')
.on('mouseover', function () {
selectedCircle = {
id: d3.select(this).attr('id'),
cls: 'nodeLeft'
}
})
.call(circleDrag)
;
currGroup
.append('circle')
.attr('class', 'node nodeRight')
.attr('id', function () {
return 'nodeRight--' + currGroup.attr('id');
})
.attr('cx', Number(op.attr('x')) + Number(op.attr('width')))
.attr('cy', op.attr('height') / 2 + Number(op.attr('y')))
// #SB: add a reference to the original cx and cy position.
// we will need it to set new cx cy when moving operator
.attr('data-cx', Number(op.attr('x')) + Number(op.attr('width')))
.attr('data-cy', op.attr('height') / 2 + Number(op.attr('y')))
//----------------------------------------------------------------------
.attr('r', 5)
.style('fill', 'red')
.on('mouseover', function () {
selectedCircle = {
id: d3.select(this).attr('id'),
cls: 'nodeRight'
}
})
.call(circleDrag)
;
Once you have done this, go to your on drag method for the operators. This is the code we are going to change:
.on('drag', function () {
var g = d3.select(this);
var currentOp = g.select('.operator');
var parent = g.select(function () {
return this.parentNode;
}).select('.qbox');
var dx = d3.event.x;
var dy = d3.event.y;
var mouse = {dx: d3.event.x, dy: d3.event.y};
var currentObj = {
x: currentOp.attr('x'),
y: currentOp.attr('y'),
width: currentOp.attr('width'),
height: currentOp.attr('height')
};
var parentObj = {
x: parent.attr('x'),
y: parent.attr('y'),
width: parent.attr('width'),
height: parent.attr('height')
};
//console.log('parent width : ' + parent.attr('width'));
//console.log('parent width : ' + currentOp.attr('width'));
//g.attr('transform', 'translate(' + x + ',' + y + ')');
var loc = getXY(mouse, currentObj, parentObj);
g.attr('transform', 'translate(' + loc.x + ',' + loc.y + ')');
d3.select('#' + g.attr('id')).selectAll('.line')[0].forEach(function (e1) {
var line = d3.select(e1);
console.log('-------------------');
console.log('line : ' + line.attr('id'));
console.log('-------------------');
var split = line.attr('id').split('__');
if(g.attr('id') == split[0]){
//change x2, y2
var otherNode = d3.select('#'+split[1]);
line.attr('x2', otherNode.attr('cx'));
line.attr('y2', otherNode.attr('cy'));
}else{
var otherNode = d3.select('#'+split[0]);
line.attr('x1', otherNode.attr('cx'));
line.attr('y1', otherNode.attr('cy'));
}
})
}))
First thing is, do not translate the whole object, only the image:
var g = d3.select(this);
var currentOp = g.select('.operator');
var parent = g.select(function () {
return this.parentNode;
}).select('.qbox');
//#SB: added a reference to the parent id
var parent_id = g.select(function () {
return this.parentNode;
}).attr('id');
//---------------------------------------
var dx = d3.event.x;
var dy = d3.event.y;
var mouse = {dx: d3.event.x, dy: d3.event.y};
var currentObj = {
x: currentOp.attr('x'),
y: currentOp.attr('y'),
width: currentOp.attr('width'),
height: currentOp.attr('height')
};
var parentObj = {
x: parent.attr('x'),
y: parent.attr('y'),
width: parent.attr('width'),
height: parent.attr('height')
};
var loc = getXY(mouse, currentObj, parentObj);
//#SB: Do not translate everything, the cx, cy values of the circle are not updated
// when translating which will make future moves calculate incorrectly
g.selectAll('image').attr('transform', 'translate(' + loc.x + ',' + loc.y + ')');
Then, instead of translating the circles, change their cx, and cy values using the original cx, cy and translate values:
g.selectAll('circle')
.attr('cx', function () {
return parseFloat(d3.select(this).attr('data-cx')) + parseFloat(loc.x);
})
.attr('cy', function () {
return parseFloat(d3.select(this).attr('data-cy')) + parseFloat(loc.y);
});
The last thing is the update to the lines. In your original code, you were selecting all lines within the operator group but you will actually miss some lines by only selecting this group. Some lines can be a part of another operator group but be connected to the operator that is moving.
In this case we should select all lines within the parent group and check if the line is connected to the operator we are moving.
If it is connected then we update the x and y values:
//#SB: Select all the lines in the parent group instead of only group of the
// operator we are moving. There can be lines that exists on other groups that
// do not exist within the group that is being moved.
d3.select('#' + parent_id).selectAll('.line')[0].forEach(function (el) {
var parent_id = g.attr('id')
var line = d3.select(el)
var nodeType = line.attr('id').split("__"); // id tells us if the line is connected to the left or right node
var operators = line.attr('class').split(" "); // class holds info on what operators the line is connected to
var sourceCircleId = nodeType[0].split("--")[0] + '--' + operators[1];
var targetCircleId = nodeType[1].split("--")[0] + '--' + operators[2];
if (parent_id == operators[1] || parent_id == operators[2]) { // the line is connected to the operator we are moving
line.attr('x1', d3.select('#' + sourceCircleId).attr('cx'))
line.attr('y1', d3.select('#' + sourceCircleId).attr('cy'))
line.attr('x2', d3.select('#' + targetCircleId).attr('cx'))
line.attr('y2', d3.select('#' + targetCircleId).attr('cy'))
}
});
Complete OnDrag code:
.on('drag', function () {
var g = d3.select(this);
var currentOp = g.select('.operator');
var parent = g.select(function () {
return this.parentNode;
}).select('.qbox');
//#SB: added a reference to the parent id
var parent_id = g.select(function () {
return this.parentNode;
}).attr('id');
//---------------------------------------
var dx = d3.event.x;
var dy = d3.event.y;
var mouse = {dx: d3.event.x, dy: d3.event.y};
var currentObj = {
x: currentOp.attr('x'),
y: currentOp.attr('y'),
width: currentOp.attr('width'),
height: currentOp.attr('height')
};
var parentObj = {
x: parent.attr('x'),
y: parent.attr('y'),
width: parent.attr('width'),
height: parent.attr('height')
};
var loc = getXY(mouse, currentObj, parentObj);
//#SB: Do not translate everything, the cx, cy values of the circle are not updated
// when translating which will make future moves calculate incorrectly
g.selectAll('image').attr('transform', 'translate(' + loc.x + ',' + loc.y + ')');
g.selectAll('circle')
.attr('cx', function () {
return parseFloat(d3.select(this).attr('data-cx')) + parseFloat(loc.x);
})
.attr('cy', function () {
return parseFloat(d3.select(this).attr('data-cy')) + parseFloat(loc.y);
});
//#SB: Select all the lines in the parent group instead of only group of the
// operator we are moving. There can be lines that exists on other groups that
// do not exist within the group that is being moved.
d3.select('#' + parent_id).selectAll('.line')[0].forEach(function (el) {
var parent_id = g.attr('id')
var line = d3.select(el)
var nodeType = line.attr('id').split("__"); // id tells us if the line is connected to the left or right node
var operators = line.attr('class').split(" "); // class holds info on what operators the line is connected to
var sourceCircleId = nodeType[0].split("--")[0] + '--' + operators[1];
var targetCircleId = nodeType[1].split("--")[0] + '--' + operators[2];
if (parent_id == operators[1] || parent_id == operators[2]) { // the line is connected to the operator we are moving
line.attr('x1', d3.select('#' + sourceCircleId).attr('cx'))
line.attr('y1', d3.select('#' + sourceCircleId).attr('cy'))
line.attr('x2', d3.select('#' + targetCircleId).attr('cx'))
line.attr('y2', d3.select('#' + targetCircleId).attr('cy'))
}
});
}))

Why does this one block of Javascript (D3) work, but not the other?

I can't tell if I'm not understanding something Javascript-specific, or if I'm not understanding how D3 is supposed to does things.
I'm simply trying to create a reusable way of generating these: http://bl.ocks.org/mbostock/5100636
The following works just fine:
function ArcGraph(selector, color) {
this.width = 80;
this.height = 80;
var arc = d3.svg.arc().innerRadius(25).outerRadius(40).startAngle(0);
var svg = d3.select("body').append("svg").attr("width", this.width).attr("height", this.height).append("g").attr("transform", "translate(" + this.width * .5 + "," + this.height * .5 + ")");
this.background = svg.append("path").datum({endAngle: tau}).style("fill", "#f5f8fd").attr("d", arc);
this.foreground = svg.append("path").datum({endAngle: .127 * tau}).style("fill", color).attr("d", arc);
this.foreground.transition().duration(750).call(arcTween, Math.random() * tau);
function arcTween(transition, newAngle) {
transition.attrTween("d", function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
}
}
But when I try and properly prototype it everything into functions, it stops working:
function ArcGraph(selector, color) {
this.width = 80;
this.height = 80;
this.arc = d3.svg.arc().innerRadius(25).outerRadius(40).startAngle(0);
this.svg = d3.select("body').append("svg").attr("width", this.width).attr("height", this.height).append("g").attr("transform", "translate(" + this.width * .5 + "," + this.height * .5 + ")");
this.background = this.svg.append("path").datum({endAngle: tau}).style("fill", "#f5f8fd").attr("d", this.arc);
this.foreground = thus.svg.append("path").datum({endAngle: .127 * tau}).style("fill", color).attr("d", this.arc);
}
ArcGraph.prototype.renderArc = function() {
this.foreground.transition().duration(750).call(arcTween, Math.random() * tau);
function arcTween(transition, newAngle) {
transition.attrTween("d", function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return this.arc(d);
};
});
}
}
The problem all lies in the "return this.arc(d)" moment. I hundreds of errors like this:
Error: Problem parsing d="MNaN,NaNA160,160 0 1,1 114.55205494059831,-111.70419288856652L71.59503433787394,-69.81512055535408A100,100 0 1,0 NaN,NaNZ"
What am I not understanding here? It's not like it doesn't see the variable 'this.arc'. Why would everything work when I declare arc as a variable, and not work in this latter case?
this.arc inside a function, which is inside a method, is not the this.arc of your ArcGraph.
Check this out: http://jsfiddle.net/58wTN/

Categories