Transition destroys SVG rotation - javascript

I am working on a JavaScript project, and as part of it, trying to rotate an svg path element around a given point.
Here is a simplified example of the js file:
var angle=0;
d3.select("#canvas")
.append("path")
.attr("id", "sector")
.attr("fill", "red")
.attr("d", "M150,150 L150,50 A100,100 0 0,1 236.6,200 Z")
.on("click", function(){
console.log("click");
angle=(angle+120)%360;
d3.select("#sector")
.transition().duration(2500) //removing this line leads to a nice transform attribute in the resulting html
.attr("transform","rotate("+angle+",150,150)");
})
And the html is just:
<svg id="canvas" width="300" height="300">
<circle cx="150" cy="150" r="100" stroke="blue" fill="none"/>
</svg>
Here you can find it on JSfiddle.
As the comment in the above excerpt suggests, it all works fine with the 3-argument version of the rotate function, where I can specify the x and y coordinates of the point which I want to use as the center of rotation. The resulting path element gets a transform attribute with the value of "rotate(120,150,150)" Unless I want to use a transition.
When I insert the line about the transition, the transformation gets some weird extra things added, it looks like "translate(354.9038105676658, 95.09618943233417) rotate(119.99999999999999) skewX(-3.1805546814635176e-15) scale(0.9999999999999999,0.9999999999999999)"
I guess in the background the non-(0,0)-centered rotation gets replaced with some translations and a (0,0)-centered rotation, just as you can do it in geometry. The position and orientation after the transition is fine indeed. However, during the transformation the element is moving on a funny path, in the example the sector leaves the circle.
Is there a way I can suppress the transition doing all these transformations and just apply a single non-(0,0)-based rotation?
Are there any other workarounds?
Changing the transform-origin for the path element attribute did not seem to work, but maybe I was doing it wrong.
I am looking for a CSS-free solution. It is an architectural decision which I cannot overrule in the project.
As you see, D3 is already involved in the project, but I would like to use as few additional external libraries as possible.
Thanks in advance!

create your arc with its center at (0,0). Then translate it to the center of the circle.
Then the d3 transition will work nicely as follows:
d3.select("#sector")
.transition().duration(2500)
.attr("transform","translate(150,150)rotate("+angle+")");

Related

Call a callback during animation [duplicate]

I have an embedded SVG in an HTML document. An (SVG) circle is animated using <animate>. I was trying to find a way to put some kind of event listener on that circle only when it moves horizontally.
Upon being moved (horizontally), I'd like to find the x-coordinates of the circle shape and set a third (outside) rect shape width to the relative position of the circle. This third rect would be like a progress bar tracking the horizontal progress of the circle.
Does the SVG circle (by the way, the circle is inside an SVG g-group) being moved by trigger some kind of event I can set a listener so that then I can change the width attribute of the sort of progress bar?
I have thought that if either the <animate> or the element moved/changed triggers some kind of event I could try to catch it and then change the width on the bar.
I have found that it is not much good use an "independent" animate on the rect as the pace of growth is very different when the circle moves upwards. I am not using the canvas element because I am trying to keep the scalability and the shapes semantics. (I would rather prefer a javascript solution but I would be grateful for other approaches.)
EDIT after answer: The anser have ben very much to the piint and (I think) helpful. I am very new to SVG and I may have misinterpreted something. Fot that reason I am including code.
I have tried to implement your recommendations and I seem to have been unsuccessful. .cx.animVal.value applied to the circle does not seem to get me what I need. I will include a chopped version of my code which should move a ball along a path which itself is being moved horizontally; two rects (inBar and outBar) should be tracking the horizontal displacement growing horizontally more or less at the same rate as the ball. In order to make sure setInterval works and the position is correctly gathered, a line has been added to list oBall..animVal and oball..baseVal. In FF 21.0, there is no change for animVal along the displacement. Have I understood your suggestions correctly? here follow the code (including headers etc. as I am a noob in SVG in particular):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<head><title>Motion</title>
<script>function beginAnim(anim,sPos){anim.beginElement();}</script>
</head>
<body>
<div id="here">
<button onclick="beginAnim(document.getElementById('anim'),'out');">START</button>
</div>
<div style="height:350px;">
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
<script type="text/ecmascript">
<![CDATA[
function trckngBars(){
oDiv=document.getElementById('here');
var oBall=document.getElementById('ball');
var oBar=[document.getElementById('inBar'),document.getElementById('outBar')];
idTimer=self.setInterval(function(){updtBars(oBall,oBar);},100);
}
function updtBars(oBall,oBar){
var xCoor=String(oBall.cx.animVal.value);
oDiv.innerHTML+='==>'+oBall.cx.animVal.value+'..'+oBall.cx.baseVal.value;
oBar[0].setAttribute("width",xCoor);
oBar[1].setAttribute("width",xCoor);
}
// ]]>
</script>
<defs>
<path id="throw" d="M0,0 q 80,-55 200,20" style="fill: none; stroke: blue;" />
</defs>
<g>
<g>
<rect x="2" y="50" width="400" height="110" style="fill: yellow; stroke: black;"></rect>
</g>
<g>
<!-- show the path along which the circle will move -->
<use id="throw_path" visibility="hidden" xlink:href="#throw" x="50" y="130" />
<circle id="ball" cx="50" cy="130" r="10" style="fill: red; stroke: black;">
<animateMotion id="ball_anim" begin="anim.begin+1s" dur="6s" fill="freeze" onbegin="trckngBars();" onend="window.clearInterval(idTimer);">
<mpath xlink:href="#throw" />
</animateMotion>
</circle>
<rect id="inBar" x="50" y="205" width="20" height="30" style="fill: purple;stroke-linecap: butt;">
<!-- <animate attributeType="XML" attributeName="width" from="0" to="200" begin="ball_anim.begin" dur="6s" fill="freeze" /> -->
</rect>
</g>
<animateTransform id="anim" attributeType="XML" attributeName="transform" type="translate" from="0" to="200" begin="indefinite" dur="10s" fill="freeze" />
</g>
<rect id="outBar" x="50" y="235" width="10" height="30" style="fill: orange;stroke-linecap: butt;">
<!-- <animate attributeType="XML" attributeName="width" from="0" to="400" begin="anim.begin+1s" dur="10s" fill="freeze" /> -->
</rect>
</svg>
</div>
</body>
</html>
If the code is run, it seems that animVal for the moving #ball remains at the same x-coordinat (50) while clearly it is moving.
An event is fired when animations begin, end or repeat but not (as you want) whenever there is a change of animation value.
As animations are deterministic though you can just start the rect shape animation so many seconds after the circle animation starts.
var cx = myCircle.cx.animVal.value;
will give you the animated value if you need it, provided that's the attribute you're animating.
You're using animateMotion rather than animating the cx and cy attributes on their own though. I'm think the only way to get the circle position post that transform is to call getBBox.
#Robert Thank you very much for your help. Your answer has been a good plunge into SVG and SMIL (and let me add cold). I have not been able to use getBBox, but inspecting the specification on paths ([link] http://www.w3.org/TR/SVG11/paths.html) and animateMotion (same site), it apears that can be achieved as SMIL animations are deterministic as suggested in your answer.
An animation has very few event triggers and by design seem as much concerned with the base state of the animation target as it is with the current position (theseem to be referred as "base values" and "presentation values"). (All the following works in javascript run by FF 21.) We can poll the current time of the animation applying getCurrentTime on the animateMotion object. I am assuming that the animation does it at constant velocity, so with that, we determine how much the object has moved along the path and obtain the length traversed (as we can get the total length of the whole path with method getTotalLength).
Then knowing the length, we can determine the current position on the path (using method getPointAtLength). Note, that the values returned, both time and position are relative to the container object, and thus they are scalable and/or require transformation).
For a (simple) working example, the javascript code in the Question sample code can be replaced by the following. It appears to work with the very few tests I have made:
function trckngBars(){
/* Upon beginning an animation (onbegin event), the required objects are gathered
and an interval is set */
var oBall=[document.getElementById('throw'),document.getElementById('ball_anim')];
var oBar=document.getElementById('inBar');
/* idTimer is set as a global variable so that it can be accessed from anywhere
to clear the interval*/
idTimer=self.setInterval(function(){updtBars(oBall,oBar);},50);
}
function updtBars(oBall,oBar){
/* This function, whose purpose is only to illustrate path method getPointLength
and animateMotion method getCurentTime, is quick and dirty. Note that oBall[0] is
the path and oBall[1] is the animate(Motion) */
//Calculates the amount of time passed as a ratio to the total time of the animation
var t_ratio=((oBall[1].getCurrentTime()-oBall[1].getStartTime())/oBall[1].getSimpleDuration());
// As mentioned, it assumes that animateMotion performs uniform motion along path
var l=oBall[0].getTotalLenth()*t_ratio;
// Gets (relative referred as user in documentation) horizontal coordinate
var xCoor=oBall[0].getPointAtLength(l).x;
oBar.setAttribute("width",xCoor);
}
function endTAnim(){
/* This function can be triggered _onend_ of an animation to clear the interval
and leave bars with the exact last dimensions */
window.clearInterval(idTimer);
var oBar=[document.getElementById('inBar'),document.getElementById('outBar')];
oBar[0].setAttribute("width",200); //hardcoded for convenience
}
Thus the simplest method I have been able to find requires the animation object (to obtain the time) and the path object (to "predict" the position) and it does not involve the actual element being moved by the animation. (It is somewhat simplifiedfrom the initial question to avoid discussing different coordinate systems when composed animations are used - this might be better discussed ia a stand-alone way.)
Though I have not noticed any lag (as the actual SVG is not much more complicated), I would be interested in knowing computationally cheaper methods as I was considering using this approach to find and draw a distance segment between two SMIL animated objects.
Of course all this relies on the assumption of a uniform movement aong the path, if that were not so and in larger images one might notice and offset I would also be grateful for any pointers on that (short of better do the animation directly in javascript/programming language and so you have total control). Thank you for all te edits you did avoiding getting into a quagmire - the only thing I knew about SVG three days ago is that it was XML.
A while ago I ran into the same problem you are describing. I wanted to be able to stop animations halfway, based on events triggered by the user and keep elements at their reached position. Unable to do so with SMIL I decided to forge my own animation system for svg.js, a small javascript library I have been working on:
http://documentup.com/wout/svg.js#animating-elements
It might be useful for what you are trying to achieve.

D3 SVG path is not filling with linear scale gradient

I have a an SVG where I am targeting a certain path element with id: _116.
I can fill the path easily with a solid colour by doing this:
d3.select("path#_116").style("fill", 'blue');
However lets say I have a linear scale colour so that I want a gradient applied to it:
let scale = d3.scaleLinear()
.domain([1,10])
.range(["white", "blue"]);
d3.select("path#_116").style("fill", scale);
This just doesn't work. For some reason it's setting the fill to a black colour (#212a39) which seems like is coming from the sibling element as shown below:
Any ideas on what's happening here?
The scale function will return a single color for a provided value (as is true for all D3 scaling functions), which likely explains the output you are seeing. The scale by default is not clamped, so the scale will interpolate values outside the domain and return values outside the range, which explains why you have a value that doesn't appear to fit in your domain.
Scaling functions do not modify the DOM but a gradient requires elements within the SVG to represent the gradients. Usually gradients are children of a defs tag:
<svg>
<defs>
<linearGradient id="gradient">
<stop stop-color="white" offset="0%"/>
<stop stop-color="blue" offset="100%"/>
</linearGradient>
</defs>
<rect width=300 height=100 fill="url(#gradient)"></rect>
</svg>
If you have an existing SVG and want to amend a single path, we can append the defs, the gradient, the stops, and apply it to a given path:
d3.xml("https://upload.wikimedia.org/wikipedia/commons/a/ab/Isobates-simple_example.svg")
.then(function(image) {
let svg = d3.select("body")
.append(()=>image.documentElement);
let gradient = svg.append("defs")
.append("linearGradient")
.attr("id","gradient");
gradient.append("stop")
.attr("stop-color","white")
.attr("offset","0%")
gradient.append("stop")
.attr("stop-color","blue")
.attr("offset","100%");
svg.selectAll("#path3156")
.style("fill", "url(#gradient)");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.0/d3.min.js"></script>
There are ways you can more efficiently do this, though generally you'll be going about achieving the same structure in the DOM (using .append() and .html() can save some lines). And D3 is completely optional as well - this is just adding a few elements to the DOM and setting an existing element's fill.
Your question appears to be concerned with a singular gradient, but if we have many gradients the answer gets a bit more complicated if we want the gradients to be data driven and incorporate scaling into the mix. Since I don't think you are asking about that I'll leave that for another time.

Labeling force-directed nodes in d3: speed vs visibility

Yet another question about adding labels to a d3 force graph...
I am labeling a graph with nodes that are inside individual groups, and I have been appending the labels inside these groups like so:
<svg>
<g class="nodes-with-labels">
<g class="individual-node">
<circle></circle>
<text>Node Label</text>
</g>
...
</g>
</svg>
This adds minimal extra elements to the graph and allows my graph's tick() function to just call one transform operation. I put up a demo fiddle here (without any movement/tick() function):
https://jsfiddle.net/52cLjxt4/1/
Unfortunately, the labels end up behind many of the nodes because they are in groups that are drawn before other groups that contain nodes. This problem can be solved by putting nodes and labels into separate parent groups, like in this example:
https://jsfiddle.net/hhwawm84/1/
<svg>
<g class="nodes">
<g class="individual-node">
<circle></circle>
</g>
...
</g>
<g class="labels">
<g class="individual-label">
<text>Node Label</text>
</g>
...
</g>
</svg>
However, this appears to be significantly slower: it creates more elements and requires two transform statements instead of one in the tick() statement, since it's moving the labels around separately.
Speed is a real concern for my project. Is there a better approach here that might avoid creating so many extra groups and doubling the transform statements?
You don't need to each label and circle in an g - just set the transform attribute directly on each element. It might also be worth profiling setting the cx/cy and x/y attributes instead of transform too.
If you don't need the graph to animate, precomputing the ticks and setting the transforms could help with performance:
for (var i = 0; i < 120; ++i) simulation.tick();
If that's still too slow, try using canvas (faster because it doesn't have a scene graph) or css transforms on html elements (faster because they are gpu accelerated).

JavaScript, SVG clippath not displaying properly after attribute change

I have an SVG-based graph. On top is a scrollable area, where the user can navigate around the graph, as shown in the image:
By dragging either of the tabs on the left/right, the user can zoom in on a particular part of the plot, as shown in the image:
Notice that only the selected portion of the graph is blue, and has a grey highlight. Both the blue and the highlight are unchanged, but are clipped using a clipping mask:
<clippath id="clip_path_scroll">
<rect id="clip_path_scroll_rect" x="x" y="y" width="w" height="h" stroke-width="0" />
</clippath>
and
<g clip-path="url(#clip_path_scroll)">
<rect id="highlighted" width="w" height="h" x="x" y="y" fill="rgba(0, 0, 0, .03)" stroke="none" />
<path id="blue_plot" d="..." stroke="rgba(26, 171, 219, 1)" stroke-width="2" fill="none"/>
</g>
And this all works fine and dandy. The clip path works flawlessly, when dragging the tabs. In that situation, it is changed incrementally, as the tab(s) slide.
But I have a reset button, that calls the following:
$('#left_scroll_group').attr('transform', 'translate(0, 0)');
$('#right_scroll_group').attr('transform', 'translate(0, 0)');
$('#clip_path_scroll_rect').attr('width', width).attr('x', x);
and when this runs, the outcome is the following:
Notice that the tab groups snap back to exactly where they should be, but it appears the clip path has only partially gone back to its original width and x-position.
BUT THIS ISN'T THE CASE
If I view the page source, and hover over the clippath rect object, it shows the following:
That is to say, it shows the rect object DID resize and move to its original position, but is not displaying properly. Moving either of the tabs immediately snaps the clippath back to working properly, as in the first two images.
Why is this happening? It happens whether the right tab is moved, or the left, or both. If the tabs are moved only a little, there doesn't seem to be much issue, but the obvious glitch/error increases as the tabs are moved further.
A similar function performs the same attr changes to the clippath, but it always shortens the path length. This glitch only seems to be happening when lengthening the length.
I am using the same types of operations on many other elements in the graph, and all work fine. All are referenced with jQuery notation (i.e. $('#element')).
I am not interested in using d3 or other libraries. I am looking for jQuery or pure JS solutions only.
EDIT 1
I've also tried
var steps = Math.floor(scrollPlotWidth - $('#clip_path_scroll_rect').attr('width'));
var curWidth = Math.ceil($('#clip_path_scroll_rect').attr('width'));
var curX = $('#clip_path_scroll_rect').attr('x');
for(var i = 0; i < steps; i++){
$('#clip_path_scroll_rect').attr('x', curX).attr('width', curWidth);
curX--;
curWidth++;
}
With no success. The changes are happening incrementally, but still no dice - it looks the same as before. This clipping issue is only happening in Safari, and seems to be fine in FireFox. Haven't tested others.
WORKING SOLUTION (HACKED)
When defining the rect within the clippath, I changed the width property. I've changed it from what is shown above to the following:
<rect id="highlighted" style="width:w;" height="h" x="x" y="y" fill="rgba(0, 0, 0, .03)" stroke="none" />
And then, the clippath functionality works properly IFF I apply the two operations:
$('#clip_path_scroll_rect').attr('x', x).css('width', w);
$('#clip_path_scroll_rect').animate(
{"width": w},
{duration: 1,
step: function(){
this.setAttribute("x", x);
}
});
Removing either of these will prevent it from working. I'm not sure why, but it will have to be a temporary fix until something better comes along. Answers are still very much welcome!

Adding SVG elements on click of other elements

I have some circles made in d3. I need to add more circles when these circles are clicked and make the existing circles disappear(they should not be removed as I will use them again).
The way I'm doing this right now is through a listener to a click event on the original circles. (these are created with .selectAll('circle.nodes')
.on("click",function(d){
//do stuff}).duration(1000);
populateSubCircles);
I want the sub circles to appear around a center (I looked at my calculations and they seem to be correct).
var populateSubCircles = function(){
var subCircles = nodesG.selectAll("circles.subNodes").data(....
This correctly adds the secondary circles, and cx and cy seem to be correct(not too far off from the center circle). However, they don't seem to appear on the page(they appear at the top left at 0,0). Why is that happening? How do I fix that?
Thanks.
EDIT-
A picture or two may help.
It's likely that you don't have the radius set on the circles (it must be set explicitly on the circle element, and cannot be set via CSS, as only style properties such as fill, stroke, etc., can be set) .
As you can see, one svg circle shows normally when set with a radius (r) of 25, and the other does not, and its size is correctly reported as 0px x 0px. (The tooltip reports size, not position). Here's the SVG I used:
<svg>
<circle cx="100" cy="100" fill="#ffdf00"
stroke="#222222" stroke-width="2px" />
<circle cx="50" cy="50" fill="#ffdf00" r="25"
stroke="#222222" stroke-width="2px" />
</svg>
This image shows the second of two circles, using inspector in Chrome web tools, it correctly reports the size:
Again, using inspector in Chrome, highlighted the first SVG element where no radius was set:
It shows the size as 0px x 0px and shows the circle as if it were in the upper left corner of the SVG document.
http://jsfiddle.net/wiredprairie/gNrZ3/

Categories

Resources