create a circle on a link between nodes on click event in d3 - d3.js

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.

Related

D3 Transition Path Left And Shift Up/Down Simultaneously

Following the tutorial of Mike Bostock on Path transitions here, I am trying to create an interpolated line chart that not only shifts through time but also transitions the y-scale / y-axis, such that is always fits to the lower and upper bounds of the data.
Some background information: The line is clipped by a clipPath and is shifted to the left whenever a new data point is added. Each new data point is added by the tick function, which also transitions the path to slide to the left.
Now the problem is, when I update the y-axis domain, it jumps to the new position. However, I would like it to smoothly transition up or down, similar to how it shifts along the x-axis. The solution probably lies in transforming the scaling of the path based on the new maximum of the data. Is there any way to achieve this or would it require a different approach by building a custom d3.interpolator() for interpolating the path?
function tick() {
// Push a new data point onto the back.
data.push(random());
// Redraw the line.
d3.select(this)
.attr("d", line)
.attr("transform", null);
// Slide it to the left.
d3.active(this)
.attr("transform", "translate(" + x(0) + ",0)")
.transition()
.on("start", tick);
// Pop the old data point off the front.
data.shift();
let max = d3.max(data, (d) => {
return d;
});
y = d3.scaleLinear()
.domain([-1, max])
.range([height, 0]);
d3.select('g .axis.axis--y').transition().duration(500).call(d3.axisLeft(y))
d3.select('g .axis.axis--x').transition().duration(500)
.attr("transform", "translate(0," + y(0) + ")")
}
I have created a jsfiddle which demonstrates the problem here.
Found a solution by using the external d3-interpolate-path library from here. Instead of transitioning the path using a transform, I interpolate the path with the old data and the path with the new data using d3.interpolatePath. Where previous is the old path and current is the new path with the newly added data point.
d3.select(this)
.attr("d", lineOld)
.attr("transform", null)
.transition().duration(500).ease(d3.easeLinear).attrTween('d', (d) => {
let previous = d3.select(this).attr('d');
let current = line(d);
return d3.interpolatePath(previous, current)
}).on("end", tick);
The Jsfiddle with my solution can be found here

d3 v6 pointer function not adjusting for scale and translate

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>

D3js Zoom With Manually Drawn Circle

I am working on a d3 scatter plot where an area of the chart will be circled (a Youden Plot). Based on available samples, I have been able to add zoom to both my data points and my axis. However, I am unable to get the circle to zoom correctly.
I suspect that I need to set up some kind of scale (scaleSqrt, possibly), but I am struggling to find documentation on this that is written at a beginner level.
My current circle code is very straightforward
var circle = drawCircle();
function drawCircle() {
return svg
.append('g')
.attr('class', 'scatter-group')
.append('circle')
.attr("r", 75 )
.attr('cx', 200 + margin.left) //suspect this needs to be related to a scale
.attr('cy', 200 + margin.top) //suspect this needs to be related to
.attr('r', 75)//suspect this needs to be related to a scale
.attr('stroke', 'red')
.attr('stroke-width', 3)
.style('fill', 'none')
}
As is the zoomed function
function zoomed() {
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
// update axes
gX.call(xAxis.scale(new_xScale));
gY.call(yAxis.scale(new_yScale));
//redraw data ppints
points.data(data)
.attr('cx', function(d) {return new_xScale(d.x)})
.attr('cy', function(d) {return new_yScale(d.y)});
//redraw circle
}
My work in progress is available in this fiddle . Can someone possible point me in the right direction?
I believe this will get you most of the way there. You need to update your circle attributes in the zoomed function along with the other elements:
function zoomed() {
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
// update axes
gX.call(xAxis.scale(new_xScale));
gY.call(yAxis.scale(new_yScale));
//redraw data ppints
points.data(data)
.attr('cx', function(d) {return new_xScale(d.x)})
.attr('cy', function(d) {return new_yScale(d.y)});
// The new part:
// the transform
let trans = d3.event.transform
// the approximate domain value of the circle 'cx' for converting later
let cx_domain = xScale.invert(200 + margin.left)
// the approximate domain value of the circle 'cy' for converting later
let cy_domain = yScale.invert(200 + margin.top)
// the circle
let circ = d3.select('.scatter-group circle')
// the radius
let rad = 75
// reset the circle 'cx' and 'cy' according to the transform
circ
.attr('cx',function(d) { return new_xScale(cx_domain)})
.attr('cy',function(d) { return new_yScale(cy_domain)})
// reset the radius by the scaling factor
.attr('r', function(d) { return rad*trans.k })
}
See this fiddle
You'll notice the circle does not scale or move at quite the same rate as the scatter dots. This is possibly because of the use of the invert function, because the conversion from range to domain and back to range is imperfect. This issue is documented
For a valid value y in the range, continuous(continuous.invert(y)) approximately equals y; similarly, for a valid value x in the domain, continuous.invert(continuous(x)) approximately equals x. The scale and its inverse may not be exact due to the limitations of floating point precision.
Your original idea to assign dynamic values to cx, cy and r will likely compensate for this, because you can then avoid the inversion.

Modifiying dimple layout grouped chart

I am using the same chart as below. I want to push the x-axis headers i.e. Regular, Premium, Budget little bit below i.e. top padding or margin. Give some styling to it like give background color and change text color. I tried using fill and it does not work as desired. I would like to hide Price Tier/Channel also
http://dimplejs.org/examples_viewer.html?id=bars_vertical_grouped
These are SVG text elements so there is no top-padding or margin. You can move them down a bit by increasing the y property though, running the following after you call the chart.draw method will move the labels down 5 pixels:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) {
// Get the y property of the current shape and add 5 pixels
return parseFloat(d3.select(this).attr("y")) + 5;
});
To change the text colour you need to use the fill property (again that's an svg text thing):
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.style("fill", "red");
To colour the background of the text is a little less trivial, there actually isn't a thing for that in SVG, however you can insert a rectangle behind the text and do what you like with it:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
// Iterate each shape matching the selector above (all the x axis labels)
.each(function () {
// Select the shape in the current iteration
var shape = d3.select(this);
// Get the bounds of the text (accounting for font-size, alignment etc)
var bounds = shape.node().getBBox();
// Get the parent group (this the target for the rectangle to make sure all its transformations etc are applied)
var parent = d3.select(this.parentNode);
// This is just the number of extra pixels to add around each edge as the bounding box is tight fitting.
var padding = 2;
// Insert a rectangle before the text element in the DOM (SVG z-position is entirely determined by DOM position)
parent.insert("rect", ".dimple-custom-axis-label")
// Set the bounds using the bounding box +- padding
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
// Do whatever styling you want - or set a class and use CSS.
.style("fill", "pink");
});
These three statements can all be chained together so the final code will look a bit like this:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) { return parseFloat(d3.select(this).attr("y")) + 5; })
.style("fill", "red")
.each(function () {
var shape = d3.select(this);
var bounds = shape.node().getBBox();
var parent = d3.select(this.parentNode);
var padding = 2;
parent.insert("rect", ".dimple-custom-axis-label")
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
.style("fill", "pink");
});
FYI the dimple-custom-axis-label class was added in a recent release of dimple so please make sure you are using the latest version. Otherwise you'll have to find an alternative selector

How to make d3 area transition originate from bottom

How can I make the transition of an area originate at the bottom of an svg rather than the top? When changing the height using this area function it will originate at the top.
area: function(width, height) {
var x = this.xScale(width),
y = this.yScale(height);
return d3.svg.area()
.x(function(d) { return x(d.x); })
.y0(height)
.y1(function(d) { return y(d.y); });
}
Transition with new height h:
var area = this.area(w, h);
svg.datum(data)
.transition()
.ease('linear', 1, .3)
.duration(1000)
.attr('d', area);
I've found this SO question but can't translate it to my problem:
D3.js Transitions
Update
Here is the code: http://jsfiddle.net/g3yS5/12/
The area a1 is situated at the top and when transitioning to a2 it pushes from top to bottom. I guess the solution to the problem involves getting the area to render to the bottom initially? If so how could I do this?
Is this what you want?
svg.select('path')
.datum(data)
.attr('transform', 'translate(0, 80)') // <---- here
.transition()
.ease('linear', 1, .3)
.duration(1000)
.attr('transform', 'translate(0, 0)') // <---- then here
.attr('d', a2);
It first translates the path to be at the bottom of the new height, then transitions that back to its natural position (NOTE: the translation is hard coded to be 80; you would want to compute it based on the height delta). This results in the path jumping to the new position, which might be what you expect.
Otherwise, you can also transition it in two steps. See this jsFiddle.
Transitions start from whatever value the attribute you're animating currently is, so you can simply change that value before the transition starts:
svg.datum(data)
.attr('d', startingArea) // set initial 'state'
.transition().ease('linear', 1, .3).duration(1000)
.attr('d', area);
In this case, startingArea could look a lot like your existing area function, but y0 would be 0 perhaps, or maybe y1 would be height. I'd have to see your code for more a more specific solution.

Resources