Running D3 v6.
This is a multi part question as in trying to solve the original problem I have a question about D3 and mouseevents. A quick note while using my fiddle, if you press the ESC key it will clear the draw line behavior.
How to draw a line from one node to another, following the cursor, regardless of zoom level and pan position?
Why does the line I draw behave differently when the .on('mousemove') is applied to an svg versus a g element?
Problem 1. The problem I am facing is that when panning and zooming, the end point of the line does not follow the cursor properly because the container I'm zooming on had it's x and y translated. Zoom in and click on a node to see the issue.
Related fiddle
This works just fine in my demo, until zooming and panning are involved. I've managed to take care of the panning issues by using d3.zoomTransform() to get the current [x,y] and apply that to the end point of the line. I cannot figure out to accommodate the zoom level though. I have tried transform(scale(zoomLevel.k)) but this doesn't work great. To recreate this issue, click a node without panning/zooming and observe the line follows the cursor. Zoom the graph and then click a node and observe the line does not follow the cursor.
Problem 2. I thought that I could solve the above issue by having the cursor react to mouse events on the g element I use for zooming and positioning rather than my parent svg element. When the mousemove event is on the g the line follows the cursor regardless of zoom/pan but is very laggy and I don't understand why.
SVG mouseevent
G mouseevent
Brief code overview, view fiddles for full code
let sourceNode;
const svg = d3.select("#chart")
.attr("viewBox", [0, 0, width, height]);
const g = svg.append('g');
const drawLine = g.append('line').attr('stroke', 'red').attr('stroke-width', 5).attr('visibility', 'hidden')
const nodes = g.append(//do node stuff)
const links = g.append(//do link stuff)
svg.call(d3.zoom().on('zoom', (event) => {
g.attr('transform', `translate(${event.transform.x}, ${event.transform.y}) scale(${event.transform.k})`)
}))
node.on('click', (event, d) => {
sourceNode = d
})
svg.on('mousemove', (event) => {
if (sourceNode) {
const currentZoom = d3.zoomTransform(svg.node());
drawLine
.attr('visibility', 'visible')
.attr('x1', sourceNode.x)
.attr('y1', sourceNode.y)
// Remove the currentZoom offset and observe the line being jank
.attr('x2', d3.pointer(event)[0] - currentZoom.x)
.attr('y2', d3.pointer(event)[1] - currentZoom.y);
}
})
Related
I'm using version 7.3.0 of d3 and I have based my code on this example. My problem is that when I'm zooming in (or out) the layout of the circles breaks and becomes
while initially was like this
The code that implements the zoom is
const zoomed = d3.zoom()
.scaleExtent([0, 3])
.on('zoom', event => {
graph.selectAll('g').attr('transform', event.transform);
});
I have noticed that if I add the following line of code, then it prevents it from breaking.
const zoomed = d3.zoom()
.scaleExtent([0, 3])
.on('zoom', event => {
graph.selectAll('g').attr('transform', event.transform);
// This line prevents zoom from breaking
simulation.restart();
});
But the downside is that it becomes noticeably slower. Is there a more efficient way to prevent it from breaking like that?
Any help would be appreciated. Thank you
I have seen examples here and here were a brush is triggered in JavaScript. I want to understand the implementation of the first one.
Background
The first example bundles two D3 line charts in a single svg container; classes focus and context, respectively:
The context chart (marked in light blue, above) is the one containing the brush, which can be triggered by a mouse click:
When we look inside its group container, we find the designated brush parameters; under the extent class:
Question 1.
I don't understand what happens in the last two lines, in particular the last line:
function drawBrush(a, b) {
// define our brush extent
// note that x0 and x1 refer to the lower and upper bound of the brush extent
// while x2 refers to the scale for the second x-axis, for context or brush area.
// unfortunate variable naming :-/
var x0 = x2.invert(a*width)
var x1 = x2.invert(b*width)
console.log("x0", x0)
console.log("x1", x1)
brush.extent([x0, x1])
// now draw the brush to match our extent
// use transition to slow it down so we can see what is happening
// set transition duration to 0 to draw right away
brush(d3.select(".brush").transition().duration(500));
// now fire the brushstart, brushmove, and brushend events
// set transition the delay and duration to 0 to draw right away
brush.event(d3.select(".brush").transition().delay(10duration(500))
}
In brush(d3.select(".brush").transition().duration(500));, the current brush parameters are selected with a transition precondition; which is passed to brush, so it can draw the new brush according to the changed brush.extend values.
In brush.event(d3.select(".brush").transition().delay(10duration(500)), it seems that the previous line sets the parameters, after which brush.event executes with the new brush parameters. Can someone make sense of this? How do the brush events apply to this case?
Question 2.
I also don't see how exactly, this event action gets linked back to the focused chart. If find the mechanisms via callbacks quite cryptic:
var brush = d3.svg.brush()
.x(x2)
.on("brush", brushed);
This snippet seems crystal-clear: the brush is made and linked to the brush event listener. On a brush event, brushed will act as the event handler. Furthermore, the scale of context's x-axis x2 is passed to the brush, as it sits on the context chart.
But I'm not quite sure how brushed works:
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
}
Just to be sure, is it correct that a new axis is generated in focus.select(".x.axis").call(xAxis); with the brush parameters set in x.domain(brush.empty() ? x2.domain() : brush.extent());?
First, there is a typo in the last line. In the code it actually is:
brush.event(d3.select(".brush").transition().delay(1000).duration(500))
Back to your question, the confusion you're facing trying to understand what the brush events have to do with it is quite simple: you're reading the D3 v4 docs, while that code uses D3 v3.
This is brush.event in D3 v3:
brush.event(selection)
If selection is a selection, it dispatches a brush gesture to registered listeners as a three event sequence: brushstart, brush and brushend. This can be useful in triggering listeners after setting the brush extent programatically. (emphasis mine)
As you can clearly see, the first line changes the brush itself (the context), while the second one changes the big area chart (the focus).
I've got a D3 radial dendrogram tree that I've applied d3.zoom to, but it jitters when I drag it. The zoom behaviour itself is fine, but the drag is not. I think there might be some issue with the way the 'g' element is translated (width / 2, height / 2 + 20).
Any help would be appreciated!
Here's a codesandbox of my tree: https://codesandbox.io/s/4zr43po6l9
change 'svg' to 'g', so the zoom affects the g element directly below the svg the zoom behaviour is attached to, and it's now smooth
let zoom = d3.zoom().on("zoom", () => {
g.attr("transform", d3.event.transform);
});
I'm not sure why it works like this to be honest
Perhaps changing the svg transform freaks out the event mouse x y position a bit as it bases its values on the svg, so you get that juddering effect?
I'm attempting to integrate AngularJS with d3 for dragging and resizing. I've managed to create a rect object that is draggable in an SVG element, and resizable using resize handles. The resize handles work as they should, but resizing is choppy when I try to resize in the north or east direction. I created the following Plunk as a demo of the issue: http://plnkr.co/tG19vpyyw0OHMetLOu2U. (I've simplified it to show the issue I've run into, so there's only one resize handle.)
Dragging works as it should, and resizing in the west and south directions works as well (not shown in the demo).
Figured I'd ask the community and see if anyone had run into this before. Thank you all.
The problem is that you're modifying the rect element itself and the enclosing g element. There's a very short delay between setting the size of the rect and the position of the g simply because this has to be done with two separate commands. During this delay, the cursor position relative the the drag rectangle changes, firing a new drag event with values that correspond to the inconsistent intermediate state. This is fixed immediately afterwards (as soon as the attributes of both elements have been adjusted) and a new drag event is fired that fixes the inconsistency, but it is noticeable as a flicker.
The easiest way to fix this is to change both size and position for the rect and nothing for the g element. This means adjusting the position of the drag rectangle as well and makes the code less nice, but avoids the timing/inconsistency problem.
So myrect becomes
var myRect = d3.select(document.createElementNS("http://www.w3.org/2000/svg", "rect"))
.attr("data-ng-width", "{{square.w}}")
.attr("data-ng-height", "{{square.h}}")
.attr("stroke", "yellow")
.attr("stroke-width", 3)
.attr("fill-opacity", 0)
.attr("data-ng-x", "{{square.x}}")
.attr("data-ng-y", "{{square.y}}");
and resizer
var resizer = myGroup.append("rect")
.attr("width", 5)
.attr("height", 5)
.attr("stroke", "blue")
.attr("stroke-width", 1)
.attr("fill-opacity", 0)
.attr("cursor", "nw-resize")
.attr("x", "{{square.x-2.5}}")
.attr("y", "{{square.y-2.5}}")
.call(nresize);
I've updated your code with this solution here.
When I use tipsy on my d3 force directed graph I have a problem: when I set the tipsy gravity to west, the tipsy begins at the upper left corner of my circle. How can I make it begin on the right side of my circle?
Here is the sample of the code I use in d3:
var node = vis.selectAll("g.node")
.data(json.nodes)
.enter().append("svg:g");
node.append("svg:circle")
.attr("r", function(d){return d.credits *5+"px";})
.style("fill", "orange");
$('svg circle').tipsy({
gravity: 'w',
html: true,
title: function() {
var d = this.__data__,
name = d.name;
return name;
}
});
Edit In this question: https://stackoverflow.com/a/10806220/1041692 they say the following:
You could try adding the tooltip to an svg:g that you overlay with the
actual circle, but give zero width and height. Currently it's taking
the bounding box and putting the tooltip at the edge. Playing around
with tipsy's options might help as well.
But either I do it wrong or it doesn't work, it didn't solve my problem.
EDIT 2 This problem also depends on the browser, in chrome the tipsy element is attached on the top left corner of the circle whereas I would like it to be attached on the middle of the right side of the circle. In Firefox, the tipsy appears on the top left of the whole webpage.
The D3 tipsy tutorial actually uses a modified version of tipsy:
http://bl.ocks.org/1373263
It is slightly tweaked to correctly calculate bounding boxes of SVG elements. So copy that source code, rather than using tipsy downloaded from the tipsy site.