I am learning D3. I know easy things like making a scatter plot and all. My next step is trying some simple interactive moves. For example, after I have an svg appended, axes and grids made, now I wish to make a circle by clicking a point within the svg canvas. I guess I will have to record coordinate of the clicked point and then append a circle with its cx nad cy, but how? How to record the coordinate?
I appreciate you show me a tutorial, give a hint or best of all an example.
If you are familiar with JQuery then D3 should have a friendly feel to it, as it shares a similar API. Specifically in regards to .on(action, fn) syntax for attaching an event listener to a DOM selection.
If you check out the jsFiddle I have created which implements a very basic implementation of what you are after then you can see this in motion in 21 lines of JS.
(function() {
var svg = d3.select('svg');
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
function drawCircle(x, y, size) {
console.log('Drawing circle at', x, y, size);
svg.append("circle")
.attr('class', 'click-circle')
.attr("cx", x)
.attr("cy", y)
.attr("r", size);
}
svg.on('click', function() {
var coords = d3.mouse(this);
console.log(coords);
drawCircle(coords[0], coords[1], getRandom(5,50));
});
})();
The most important aspects of this snippet are on lines 18-20 (.on(...) and d3.mouse(this)). The on('click',..) event is attached to the svg element. When a click occurs d3.mouse is called with the current scope as its argument. It then returns an array with x and y coords of the mouse event. This information is then passed to drawCircle, along with a random radius to draw a circle on the current SVG canvas.
I advise you to take the jsFiddle and have a play!
Related
I am upgrading my app from d3 v5 to v6 and am having an issue migrating the d3.mouse functionality. In my app I apply a transform to the top level svg group and use the zoom functionality to zoom and pan (scale and translate). When I double click on the screen I take the mouse position and draw a square.
Now I am replacing the d3.mouse function with d3.pointer. In my double click event I get the mouse position by calling d3.pointer(event). However this function is not producing a position that is relative to where my top level svg group is positioned and scaled. When I remove the translate and scale from the top level group, the position matches up.
In the older version of d3 I could call d3.mouse(this.state.svg.node()) and it would produce the exact position I clicked corrected for pan and scale. Is this available in version 6? If not, is there a clean way I can adjust for this? The new event object is coming through with a host of different position properties: pagex, offsetx, screenx, x. None of these is producing the position I clicked on. Is there a clean way to acheive this?
You could specify a container element which would factor in a zoom transform in v5 and earlier:
d3.mouse(container)
Returns the x and y coordinates of the current event relative to the specified container. The container may be an HTML or SVG container element, such as a G element or an SVG element. The coordinates are returned as a two-element array of numbers [x, y]. (source)
In d3v6 you can specify this by using the second parameter of d3.pointer:
d3.pointer(event[, target])
Returns a two-element array of numbers [x, y] representing the coordinates of the specified event relative to the specified target. event can be a MouseEvent, a PointerEvent, a Touch, or a custom event holding a UIEvent as event.sourceEvent.
...
If the target is an SVG element, the event’s coordinates are transformed using the inverse of the screen coordinate transformation matrix. If the target is an HTML element, the event’s coordinates are translated relative to the top-left corner of the target’s bounding client rectangle. (source)
So as far as I'm aware, you should be use:
d3.pointer(event,this.state.svg.node());
Instead of
d3.mouse(this.state.svg.node());
Here's a d3v6 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function(event) {
g.attr("transform", event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function(event) {
var xy = d3.pointer(event,g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
Adapting this v5 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function() {
g.attr("transform", d3.event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function() {
var xy = d3.mouse(g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I use dc.js lineChart and barChart. Now I need to mark the maximum and minimum values on my lineChart with 'renderArea(true)'.
I want something like in the picture below or maybe something else, but I don't know how to add this feature.
Update:
Gordon's answer is perfect. Unfortunately, my chart doesn't show the hint with 'mouseover' on marked points
One more update:
How can I redraw these points after zooming?
This isn't something supported directly by dc.js, but you can annotate the chart with a renderlet. Gladly, dc.js makes it easy to escape out to d3 when you need custom annotations like this.
We'll use the fact that by default the line chart draws invisible dots at each data point (which only appear when they are hovered over). We'll grab the coordinates from those and use them to draw or update our own dots in another layer.
Usually we'd want to use a pretransition event handler, but those dots don't seem to have positions until after the transition, so we'll have to handle the renderlet event instead:
chart.on('renderlet', function(chart) { // 1
// create a layer for the highlights, only once
// insert it after the tooltip/dots layer
var highlightLayer = chart.select('g.chart-body') // 2
.selectAll('g.highlight-dots').data([0]);
highlightLayer
.enter().insert('g', 'g.dc-tooltip-list').attr('class', 'highlight-dots');
chart.selectAll('g.dc-tooltip').each(function(_, stacki) { // 3
var dots = d3.select(this).selectAll('circle.dot'); // 4
var data = dots.data();
var mini = 0, maxi = 0;
data.forEach(function(d, i) { // 5
if(i===0) return;
if(d.y < data[mini].y)
mini = i;
if(d.y > data[maxi].y)
maxi = i;
});
var highlightData = [mini, maxi].map(function(i) { // 6
var dot = dots.filter(function(_, j) { return j === i; });
return {
x: dot.attr('cx'),
y: dot.attr('cy'),
color: dot.attr('fill')
}
});
var highlights = highlightLayer.selectAll('circle.minmax-highlight._' + stacki).data(highlightData);
highlights
.enter().append('circle') // 7
.attr({
class: 'minmax-highlight _' + stacki,
r: 10,
'fill-opacity': 0.2,
'stroke-opacity': 0.8
});
highlights.attr({ // 8
cx: function(d) { return d.x; },
cy: function(d) { return d.y; },
stroke: function(d) { return d.color; },
fill: function(d) { return d.color; }
});
});
});
This is fairly complicated, so let's look at it step-by-step:
We're listening for the renderlet event, which fires after everything has transitioned
We'll create another layer. The .data([0]).enter().insert(stuff) trick is a degenerate case of the d3 general update pattern that just makes sure an item is added exactly once. We specify the selector for the existing tooltip/dots layer as the second parameter to .insert(), in order to put this layer before in DOM order, which means behind. Also, we'll hold onto the update selection because that is either the inserted node or the existing node.
We iterate through each of the stacks of tooltip-dots
In each stack, we'll select all the existing dots,
and iterate over all their data, finding the minimum and maximum indices mini and maxi.
Now we'll create a two-element data array for binding to the min/max highlight dots, pulling data from the existing dots
Now we're finally ready to draw stuff. We'll use the same degenerate update pattern to draw two dots with class minmax-highlight _1, _2, etc.
And use the color and positions that we remembered in step 6
Note that the min and max for each stack is not necessarily the same as the total min and max, so the highlighted points for a higher stack might not be the highest or lowest points.
Not so simple, but not too hard if you're willing to do some d3 hacking.
Example fiddle: http://jsfiddle.net/gordonwoodhull/7vptdou5/31/
Want to add a circle on the links between nodes on click and I should be able to attach a drag event to the circle so that when I drag a circle, the link should move to . where I am going wrong in this?
var dragCircle = d3.behavior.drag()
.on('dragstart', function(){
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function(d,i){
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
});
//I want to attach circle to the link so that when I drag circle, line should move too.
function drawCircle(x, y, size) {
svg.selectAll(".edge").append("circle")
.attr('class', 'linkcirc')
.attr("cx", x)
.attr("cy", y)
.attr("r", size)
.style("cursor", "pointer")
.call(dragCircle);
}
//catching the mouse position to decide to place the circle
edge.on("click",function() {
var coords = d3.mouse(this);
drawCircle(coords[0], coords[1],3);
});
SVG will not allow you to create a circle as a child of a line (and your code is creating one circle for every link on every click). Instead of this:
svg.selectAll(".edge").append("circle") # appends one circle to each edge
Try this:
svg.append("circle") # appends a single circle to the SVG image
After changing your fiddle accordingly I was able to fire the drag event, but it still needs work. Using the drag behaviour you probably want to look at the event.dx and event.dy values rather than the absolute values, and you can simply change the circle's cx and cy instead of applying a translation (if that's easier). See https://jsfiddle.net/pzej8tkq/3/ for ideas.
I'm trying to get the screen position of a node after the layout has been transformed by d3.behavior.zoom() but I'm not having much luck. How might I go about getting a node's actual position in the window after translating and scaling the layout?
mouseOver = function(node) {
screenX = magic(node.x); // Need a magic function to transform node
screenY = magic(node.y); // positions into screen coordinates.
};
Any guidance would be appreciated.
EDIT: 'node' above is a force layout node, so it's x and y properties are set by the simulation and remain constant after the simulation comes to rest, regardless of what type of transform is applied.
EDIT: The strategy I'm using to transform the SVG comes from d3's zoom behavior, which is outlined here: SVG Geometric Zooming.
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) { return "translate(" + d + ")"; });
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
It's pretty straightforward. d3's zoom behavior delivers pan and zoom events to a handler, which applies the transforms to the container element by way of the transform attribute.
EDIT: I'm working around the issue by using mouse coordinates instead of node coordinates, since I'm interested in the node position when the node is hovered over with the mouse pointer. It's not exactly the behavior I'm after, but it works for the most part, and is better than nothing.
EDIT: The solution was to get the current transformation matrix of the svg element with element.getCTM() and then use it to offset the x and y coordinates to a screen-relative state. See below.
It appears the solution to my original question looks something like this:
(Updated to support rotation transforms.)
// The magic function.
function getScreenCoords(x, y, ctm) {
var xn = ctm.e + x*ctm.a + y*ctm.c;
var yn = ctm.f + x*ctm.b + y*ctm.d;
return { x: xn, y: yn };
}
var circle = document.getElementById('svgCircle'),
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
ctm = circle.getCTM(),
coords = getScreenCoords(cx, cy, ctm);
console.log(coords.x, coords.y); // shows coords relative to my svg container
Alternately, this can also be done using the translate and scale properties from d3.event (if rotation transforms are not needed):
// This function is called by d3's zoom event.
function zoom() {
// The magic function - converts node positions into positions on screen.
function getScreenCoords(x, y, translate, scale) {
var xn = translate[0] + x*scale;
var yn = translate[1] + y*scale;
return { x: xn, y: yn };
}
// Get element coordinates and transform them to screen coordinates.
var circle = document.getElementById('svgCircle');
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
coords = getScreenCoords(cx, cy, d3.event.translate, d3.event.scale);
console.log(coords.x, coords.y); // shows coords relative to my svg container
// ...
}
EDIT: I found the below form of the function to be the most useful and generic, and it seems to stand up where getBoundingClientRect falls down. More specifically, when I was trying to get accurate SVG node positions in a D3 force layout project, getBoundingClientRect produced inaccurate results while the below method returned the circle element's exact center coordinates across multiple browsers.
(Updated to support rotation transforms.)
// Pass in the element and its pre-transform coords
function getElementCoords(element, coords) {
var ctm = element.getCTM(),
x = ctm.e + coords.x*ctm.a + coords.y*ctm.c,
y = ctm.f + coords.x*ctm.b + coords.y*ctm.d;
return {x: x, y: y};
};
// Get post-transform coords from the element.
var circle = document.getElementById('svgCircle'),
x = +circle.getAttribute('cx'),
y = +circle.getAttribute('cy'),
coords = getElementCoords(circle, {x:x, y:y});
// Get post-transform coords using a 'node' object.
// Any object with x,y properties will do.
var node = ..., // some D3 node or object with x,y properties.
circle = document.getElementById('svgCircle'),
coords = getElementCoords(circle, node);
The function works by getting the transform matrix of the DOM element, and then using the matrix rotation, scale, and translate information to return the post-transform coordinates of the given node object.
You can try node.getBBox() to get the pixel positions of a tight bounding box around the node shapes after any transform has been applied. See here for more: link.
EDIT:
getBBox doesn't work quite the way I thought. Since the rectangle is defined in terms of the transformed coordinate space it is always relative to the parent <g> and will therefore always be the same for contained shapes.
There is another function called element.getBoundingClientRect that appears to be quite widely supported and it returns its rectangle in pixel position relative to the top left of the browser view port. That might get you closer to what you want without needing to mess with the transform matrix directly.
I'm trying to find a way to reset the zoom property of my svg whenever I click on an object and leave it as is. For example in this jsfiddle if you zoom out and click on a square, the zoom gets reset but then when I try to pan the screen the zoom goes back to what it was before I clicked on the square. Is there a way such that when I click on a square, the zoom goes back to 100% and stays at 100% even when panning?
JSFiddle: http://jsfiddle.net/nrabinowitz/p3m8A/
Here is my zoom call:
svg.call(d3.behavior.zoom().on("zoom", redraw));
The key is to tell the zoom behaviour that you're resetting scale and translation. To achieve this, simply save it in a variable and set the values appropriately as you're setting the transform attribute of the SVG.
var zoom = d3.behavior.zoom();
svg.call(zoom.on("zoom", redraw));
// ...
.on("click", function () {
counter++;
canvas
.transition()
.duration(1000)
.attr('transform', "translate(" + (offset * counter) + ",0)");
zoom.scale(1);
zoom.translate([offset * counter, 0]);
drawBox(x, y);
});
Updated jsfiddle here.