d3 drag 'on' event handler only has one event argument - d3.js

I'm working with d3 version 7.4.0 and I noticed that the callback/handler for the 'on' event in the drag behavior only has a single argument, contrary to what I read in the docs https://github.com/d3/d3-drag. I was wondering how I can implement a simple drag and drop of an svg circle. Thank you.
this.player = this.container
.append("circle")
.attr("cx", 1000)
.attr("cy", 1000)
.attr("r", 20)
.attr("fill", "pink")
.call(
<any>(
d3
.drag<SVGCircleElement, DragEvent>()
.on("start", function (event: DragEvent, d: any) {
console.log("e>>", event);
console.log("d>>", d); // d is undefined
d3.select(this).attr("cx", event.x).attr("cy", event.y); // I would probably want to do something like attr('cx', d.x + event.x)
// this.cx = event.x;
})
)
);
from docs:
function dragged(event, d) {
circle.raise().attr("cx", d.x = event.x).attr("cy", d.y = event.y);
}

Related

Force directed graph's gravity that forms a rectangle

I would like to make so that my nodes fit a rectangle shaped space instead of a circle (default gravity).
I have read a similar topic here but was unable to make it work with my code.
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
forceNode.strength(-150);
forceLink.strength(1);
forceLink.distance(50)
const simulation = d3.forceSimulation(nodes)
.force(link, forceLink)
.force("charge", d3.forceManyBody().strength(-150))
.force("collide", d3.forceCollide(10).strength(10).iterations(1))
.force('x', d3.forceX(width/4).strength(1))
.force('y', d3.forceY(height/4).strength(10))
.on("tick", ticked);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";});
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart(); //comment to remove sim
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0); //comment to remove sim
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Then I apply ".call(drag(simulation));" to my node element
When the map loads, the nodes and links are all cramed into a vertical line, then after a few seconds and a few lag spikes they end up forming a sort of oval. The map remains messy and unreadable.
Any clue what I am doing wrong?

Find distance of drag in D3

I've got a set of D3 elements (they happen to be text nodes, but I think it doesn't matter what kind) that I've attached drag behavior to:
paragraphs.enter().append("text")
.text(function (d, i) { return d })
.attr("x", function (d, i) { return (i + 1) * 32 })
.attr("y", function (d, i) { return (i + 1) * 16 })
.attr("fill", color)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
I'd like the drag behavior to be aware of how far the user has dragged since the dragstart. Currently, I add an attr in dragstarted which stamps the initial position, then during dragged check for current position and calculate it that way.
Is there a way to find the distance dragged since the drag start without calculating it, simply from the drag events directly? I've looked at dx/dy, but they seem to calculate only since the last drag event, so the value is always in the low single digits.
According to the API, those are the fields exposed by the event object during the drag event:
target - the associated drag behavior.
type - the string “start”, “drag” or “end”; see drag.on.
subject - the drag subject, defined by drag.subject.
x - the new x-coordinate of the subject; see drag.container.
y - the new y-coordinate of the subject; see drag.container.
dx - the change in x-coordinate since the previous drag event.
dy - the change in y-coordinate since the previous drag event.
identifier - the string “mouse”, or a numeric touch identifier.
active - the number of currently active drag gestures (on start and end, not including this one).
sourceEvent - the underlying input event, such as mousemove or touchmove.
As you can see, none of them holds the value that you are looking for.
However, there is a simple solution, which may not suit you since you clearly stated "without calculating it".
The solution involves getting the position at the "start" listener and comparing it with the position at the "end" listener:
d3.drag().on("start", function(d) {
d.startX = d.x;
d.startY = d.y;
}).on("drag", function(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}).on("end", function(d) {
console.log("x distance: " + (d.x - d.startX))
console.log("y distance: " + (d.y - d.startY))
})
This is a simple demo, drag the circle around:
var svg = d3.select("svg");
var circle = svg.append("circle")
.datum({
x: 150,
y: 75
})
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.attr("r", 10)
.attr("fill", "teal");
circle.call(d3.drag().on("start", function(d) {
d.startX = d.x;
d.startY = d.y;
}).on("drag", function(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}).on("end", function(d) {
console.log("x distance: " + (d.x - d.startX))
console.log("y distance: " + (d.y - d.startY))
}))
svg {
border: 1px solid black;
}
.as-console-wrapper { max-height: 25% !important;}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Again: I'm aware that you said "no calculation", but since there is no native property for what you want I thought it would be worth showing how simple it is doing the calculation.

Integrating with canvas drawings in nodes

I'm attempting to draw a graph/network in which node contents should be filled with a third party library. So far, I was able to draw the graph with d3 with a container () for each node to be filled later on. My problem is that the containers seem not to yet exist when there are referred for drawing. Adding an onload event doesn't work either, but using a onclick event shows that everything is in place to work and most probably drawing starts before the DOM elements are actually created.
What is the best practice to make sure d3 generated DOM elements are created before starting assigning them content? The third party JS library I use only requires the div id to draw content in.
From arrays of nodes and relationships return by Neo4j, I build a graph with d3 as follow. Images to be displayed in nodes are stored in the neo4j data base as Base64 strings.
var width = 500, height = 500;
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var col = obj.columns;
var data = obj.data;
var nodes = obj.data[0][0].nodes;
var nodeMap = {};
nodes.forEach(function(x) { nodeMap[x.label] = x; });
var edges = obj.data[0][0].edges;
var links = edges.map(function(x) {
return { source: nodeMap[x.source], target: nodeMap[x.target], value: x.value };
});
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "all")
.append('g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('g');
var force = d3.layout.force()
.gravity(.3)
.distance(150)
.charge(-4000)
.size([width, height]);
var drag = force.drag()
.on("dragstart", dragstart);
force
.nodes(nodes)
.links(links)
.friction(0.8)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("svg:g")
.attr("class", "node")
.on("dblclick", dblclick)
.call(force.drag);
node.append("circle")
.attr("r", 50);
node.append("image")
// display structure in nodes
.attr("xlink:href", function(d){
if (d.imgB64) {
return 'data:image/png;base64, ' + d.imgB64 ;
}
})
.attr("x", -40)
.attr("y", -40)
.attr("width", 80)
.attr("height", 80);
force.on("tick", function() {
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 + ")"; });
});
That code works fine. Hence, what I'm attempting to do is to replace the Base64 images with canvas drawings generated by a third party library.
This library works as follow:
var myCanvas = newCanvasViewer(id, data);
This function knows how to generate an image from the given data and puts the result in the DOM element, if the element already exists in the DOM, or generate a new canvas element in the DOM, directly in but unfortunately erasing any other existing DOM elements.
So, I changed the d3 above code, replacing the node.append('image') block with:
node.append("canvas")
.attr("id", function(d) {
var cnv = newCanvasViewer(d.name, d.data);
return d.name;
});
This obviously doesn't work, canvas objects are displayed but not in the nodes, probably because the DOM element doesn't exist yet when calling newCanvasViewer. Furthermore, the d3 graph is overwritten.
When setting an onclick function calling newCanvasViewer, the drawing shows up within the nodes on click.
node.append("circle")
.attr("r", 50)
.attr("onclick", function(d) {
return "newCanvasViewer('"+d.name+"', '"+d.data+"')";
});
node.append("canvas")
.attr("id", function(d) {
return d.name;
});
Since I would like each node to display its canvas drawing from start, I was wondering when to call the newCanvasViewer function? I guess an oncreate function at each node would make it, but doesn't exist.
Would a call back function work? Should I call it once d3 is finished with the whole network drawing?
In order to be more comprehensive, here is the HTML and javascript code with callback function to attempt drawing canvas content once d3 d3 is done drawing. I'm still stuck, the canvas sample is actually displayed but no canvas content show in nodes, probably due to timing between generating canvas containers and using them.
HTML:
<body onload="init(); cypherQuery()">
<div id="graph" align="left" valign="top"></div>
<div id="sample">
<canvas id="mol" style="width: 160px; height: 160px;"></canvas>
</div>
</body>
Javascript (in the HTML header):
var xmlhttp;
function init() {
xmlhttp = new XMLHttpRequest();
}
function cypherQuery() {
// perform neo4j query, extract JSON and call the network drawing (d3).
// ...
var obj = JSON.parse(xmlhttp.responseText);
drawGraph(obj, drawCanvas);
// this is just to show that canvasViewer does the
// job if the <canvas id='...'> element exists in DOM
var nodes = obj.data[0][0].nodes;
var cnv = newCanvasViewer("sample", nodes[0].data);
}
function drawGraph(obj, drawCanvas) {
// see code above from...
var width = 500, height = 500;
// ... to
force.on("tick", function() {
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 + ")";
});
});
function redraw() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
// node.attr("font-size", (nodeFontSize / d3.event.scale) + "px");
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
if (callback && typeof(callback) === "function") {
callback(nodes);
}
}

D3 drag error 'Cannot read property x of undefined'

I'm trying to get drag functionality to work on D3, and have copied the code directly from the developer's example.
However it seems the origin (what is being clicked) is not being passed correctly into the variable d, which leads to the error: 'Cannot read property 'x' of undefined'
The relevant code:
var drag = d3.behavior.drag()
.on("drag", function(d,i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d.x,d.y ] + ")"
})
});
var svg = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 300);
var group = svg.append("svg:g")
.attr("transform", "translate(10, 10)")
.attr("id", "group");
var rect1 = group.append("svg:rect")
.attr("rx", 6)
.attr("ry", 6)
.attr("x", 5/2)
.attr("y", 5/2)
.attr("id", "rect")
.attr("width", 250)
.attr("height", 125)
.style("fill", 'white')
.style("stroke", d3.scale.category20c())
.style('stroke-width', 5)
.call(drag);
Usually, in D3 you create elements out of some sort of datasets. In your case you have just one (perhaps, one day you'll want more than that). Here's how you can do it:
var data = [{x: 2.5, y: 2.5}], // here's a dataset that has one item in it
rects = group.selectAll('rect').data(data) // do a data join on 'rect' nodes
.enter().append('rect') // for all new items append new nodes with the following attributes:
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
... // other attributes here to modify
.call(drag);
As for the 'drag' event handler:
var drag = d3.behavior.drag()
.on('drag', function (d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this)
.attr('transform', 'translate(' + d.x + ',' + d.y + ')');
});
Oleg's got it, I just wanted to mention one other thing you might do in your case.
Since you only have a single rect, you can bind data directly to it with .datum() and not bother with computing a join or having an enter selection:
var rect1 = svg.append('rect')
.datum([{x: 2.5, y: 2.5}])
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
//... other attributes here
.call(drag);

D3 transform transition "null parsing transform attribute"

The basic problem is that I have a drag event on a path that transitions the path a certain distance in the x axis.
When another event happens I want to return the path back to its original position before the drag event.
I tried to do this with the below code:
svg.select('#' + uniqueid + '-flare')
.select('path')
.transition().duration(1000)
.ease('linear')
.attr('transform', 'translate(0,0)');
This is giving me the warning "null parsing transform attribute" and the SVG container in the HTML shows transform="".
Using this following code works perfectly well, but is choppy. I want the smooth animation provided by the transition.
svg.select('#' + uniqueid + '-flare')
.select('path')
.attr('transform', 'translate(0,0)');
Any ideas? Thanks!
Update
Lars asked to see some more code. So.... (It is very abridged)
var dragtype, dragsum;
var wordDrag = d3.behavior.drag()
.origin(Object)
.on('drag', function (d) {
if(dragType != d.word) {
dragType = d.word;
dragSum = 0;
}
//update current position
dragSum = dragSum + parseInt(d3.event.dx);
var xMovement = parseInt(this.getAttribute('x')) + parseInt(d3.event.dx);
d3.select(this)
.attr('class', 'dragged-points')
.attr('x', xMovement);
if(uniqueId == d.word) {
svg.select('#' + uniqueId + '-flare')
.select('path')
.attr('transform', 'translate(' + dragSum + ',0)');
}
}
});
word.selectAll('text')
.data(data)
.enter().append('text')
.attr('class', 'points')
.attr('id', ... )
.attr('x', ...)
.attr('y', ...)
.call(wordDrag);
tick = setInterval(function(){
animate();
}, 1000);
function animate() {
word.transition()
.duration(1000)
.ease('linear')
.attr('x', ...)
.attr('y', ...);
svg.select('#' + uniqueId + '-flare')
.select('path')
.transition().duration(1000)
.ease('linear')
.attr('transform', 'translate(0,0)');
}

Resources