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
Related
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.
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've got a project using this Voronoi Tessellation plugin with series of coordinates representing locations of temperature sensors - I'm thinking of using JSON to represent their locations and detected temperature values.
What I need to is to display the temperature value of the sensor(point) I'm referencing to(the area where I'm mouse-hovering) when mouse-hovering an area.
https://github.com/mbostock/d3/wiki/Voronoi-Geom
I've been reading this documentation again and again but still can't figure out whether detecting which specific point that the mouse-hovering area belongs to is possible.
Has anyone tried this before? Are there good examples about it?
If I'm understanding your question, you want to display text at the vertices point when a user mouses into the voronoi partition?
You could do this by handling the mouseenter/leave events of each path:
path.enter().append("path")
.attr("class", function(d, i) {
return "q" + (i % 9) + "-9";
})
.attr("d", polygon)
.on("mouseenter", function(d,i){
if (!someTexts[i]) { // get some fake value
someTexts[i] = (Math.random()*100).toFixed(1);
}
// append text
currentText = svg.append("text")
.text(someTexts[i])
.attr("transform","translate(" + vertices[i] + ")")
.attr("text-anchor","middle")
.attr("alignment-baseline", "middle");
})
.on("mouseleave", function(d,i){
// remove text
currentText.remove();
});
Example here.
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.
For a project, I need to interactively change hierarchical data layout of a visualization - without any change of the underlying data whatsoever. The layouts capable of switching between themselves should be tree, cluster, radial tree, and radial cluster. And transitioning should be preferably an animation.
I thought that would be relatively easy task with D3. I started, but I got lost in translations and rotations, data bindings, and similar, so I am asking you for help. Also, probably I am doing something not in the spirit of D3, which is bad since I am seeking a clean solution.
I put together a jsfidle, but it is just a starting point, with added radio buttons, convenient small data set, and initial cluster layout - just to help anybody who wants to take a look at this. Thanks in advance!
UPDATE:
I wanted to focus on links only, so I temporary disabled other elements. Building on #AmeliaBR method, following animations are obtained:
Here is updated jsfiddle.
UPDATE 2:
Now with circles: (excuse my choice of colors)
{doom-duba-doom}
Here is one more updated jsfiddle.
I don't see why it would be that hard so long as all your layouts have the same overall structure of link-paths, circle nodes and text labels.
Just make sure all your objects, including your link paths, have a good data-key that is independent of the data attributes created by the layout functions. Then for each transition, update the data with the results of the appropriate layout function and draw that layout.
I've got the transition to radial tree implemented here: http://jsfiddle.net/YV2XX/5/
Key code:
//Radial Tree layout//
var diameter = 500;
var radialTree = d3.layout.tree()
.size([360, diameter / 2 ])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
var radialDiagonal = d3.svg.diagonal.radial()
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
function transitionToRadialTree() {
var nodes = radialTree.nodes(root), //recalculate layout
links = radialTree.links(nodes);
svg.transition().duration(1500)
.attr("transform", "translate(" + (diameter/2)
+ "," + (diameter/2) + ")");
//set appropriate translation (origin in middle of svg)
link.data(links, function(d){
return d.source.name + d.target.name;})
.transition().duration(1500)
.attr("d", radialDiagonal); //get the new radial path
node.data(nodes, function(d){
return d.name ;})
.transition().duration(1500)
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
node.select("circle")
.transition().duration(1500)
.attr("r", 4.5);
node.select("text")
.transition().duration(1500)
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; });
};
The layout code is all from http://bl.ocks.org/mbostock/4063550, I've just changed it to be an update instead of an initialization.
Also note that I have moved the variable declaration for root outside of the data-reading method, so it can be re-accessed by the transition functions.
Layout still needs some finessing, but you get the idea.
Now, if you wanted one of the transitions to be a partition, treemap or other layout that doesn't use the node-link structure, they it gets more complicated...
I don't have enough reputation to make a comment...so, I am just giving this tiny contribution as a pseudo-answer. After looking at this post, and based on #VividD's perfect comment on how simple the transitions turned out to be, I simply added the Tree Vertical option to the transformations in this fiddle.
The addition is simply this:
var diagonalVertical = d3.svg.diagonal()
.projection(function (d) {
return [d.x, d.y];
});
Anyways, I have bookmarked this highly instructional interaction.