Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
david_stocker
Advisor
Advisor
0 Kudos

This is part of a tutorial series on creating extension components for Design Studio.

Last time, we ensured that we were binding data to our shapes.  Now were going to get down to animating things.  Animation in D3 is gone through transitions.  In short, a transition in D3 changes an attribute on an existing component over time.  If that attribute is part of a path, you can alter the position or shape over time.  If it is a style element, you can slide from one style property to another.  The possibilities are limitless.  Furthermore, if what you are doing is simple, D3 can take care of it for you, black box style.  If it is more complex, you can take manual control.

10,000 Foot View of Transitions

Let's take a very simple example.  In the html code below, we use D3 to draw a small, red, semi-transparent circle.


<!DOCTYPE html>


<html>


  <head>


  <title>Simple D3 Transition</title>


  <div id='content'></div>


  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>


    </head>


  <body class='sapUiBody'>


  <script>


  var vis = d3.select("#content").append("svg:svg")


  var circle = vis.append("circle")


  .attr("cx", 50)       


  .attr("cy", 50)      


  .attr("r", 20)


  .attr("opacity", 0.2)


  .style("fill", "red");


  </script>


  </body>


</html>





D3 transitions have a fairly straightforward lifecycle.

  1. It is scheduled
  2. It starts
  3. It runs
  4. It Ends

The scheduling might be right away (the default) or it might be delayed.  Scheduling a transition is automatically triggered by selecting what we want to transition and calling it's transition() function; in the case above, it would be:


circle.transition();





The line of code above would not do anything visually, but it would trigger the full transition lifecycle.  Start and End are proper events and we could attach callbacks to them and do whatever we want on those events.  We could, for example, trigger browser alerts, telling the user that we're starting and ending the transition:


circle.transition()


  .each("start", function() { alert("start");})


  .each("end", function() { alert("end");});





This would trigger the two popups as soon as the page is refreshed.  We could also schedule a delay into the start of the transition by a couple of seconds (2000miliseconds = two seconds:


circle.transition()


  .delay(2000)


  .each("start", function() { alert("start");})


  .each("end", function() { alert("end");});





We can also define how long the transition lasts.  So in the example below, it is scheduled to start two seconds after refresh (when the start alert appears), with a duration of 5 seconds.  So the end alert comes seven seconds after page refresh.


circle.transition()


  .delay(2000)


  .duration(5000)


  .each("start", function() { alert("start");})


  .each("end", function() { alert("end");});





We've covered the scheduling, start and end.  Now let's have a look at the running.

Keyframes and Tweening

In the days of hand drawn animation, Disney's more experienced artists would not draw all 24 frames per second.  Instead, they would draw points in an animation; usually points with relatively simple movement in between.  These were called key frames.  Younger, less experienced (and less expensive) artists would fill in the frames in between the keyframes; this process was called inbetweening, or tweening for short.  This process is also used in computer animation, both of the 2D and 3D sort.  When preparing animations for a computer game for instance, a 3D artist will define the keyframes of an actor's motion.  At runtime, the graphics engine will perform the tweening computationally; providing smooth motion between the keyframes.

Transitions in D3 are also keyframe based.  There are always two keyframes; a start and an end keyframe.  D3 uses a built in tweening function to orchestrate the timing of frames and the update of properties.  In turn, it uses an interpolator function to determing the transient values the in between timeslice frames of an animation. The interpolation function is triggered once per frameand terurns a calculated value for the  attribute or style in question.  D3 contains a number of built in interpolator functions and will try to use one of these, depending on the data type being transitioned.

D3 has built in interpolators for handling cases such as:

  • numbers
  • colors
  • geometric transforms, such as scale, rotation and transformation
  • certain kinds of strings

If you need a custom interpolator, you can override the standard attrTween or styleTween and implement your own, along with the custom interpolator.

Using Standard attrWeen()

Our gauge rotates about its origin point and lends itself to a straightforward animation, using D3's built in rotation interpolator.  We simple add an attribute update to the transition() function and ask it to perform a transformation for rotation, with a specified end angle:


//Arcs are in radians, but rotation transformations are in degrees.  Kudos to D3 for consistency


needle.transition()


  .attr("transform", "rotate(" + endAngleDeg + ")")


  .duration(durationArc)


  .delay(delayArc)


  .ease(easeArc);





Opacity , being defined by a number between zero and one, is another thing that can be handled by a built in D3 attribute interpolator.  Here, we chain two transitions together.  The first instantaneously sets the opacity of the gauge's outer ring to 0 (fully transparent).  The second schedules a fade in that brings the "fill-opacity" back to 1; making the outer ring fully opaque.


ringArc.transition()


  .attr( "fill-opacity", 0 )


  .transition()


  .delay( delayArc )


  .duration(durationArc)


           .attr( "fill-opacity", 1 );





A custom arcTween()

As D3 has no built in radial interpolator for arcs the standard attrTween() function won't know how to handle it.  Therefore, we'll need to override the standard attrTween() function.  We don't need to override the standard interpolate() function, as we'll use the angles as let it operate as a standard linear interpolator.  The attrTween() function will take the interpolated  angle at each frame and update arc, arcDef.


guageArc.transition()


  .duration(durationArc)


      .attrTween("d", function(d) {


     var interpolate = d3.interpolate(d.endAngle, endAngleDeg * (pi/180));


     return function(t) {


  d.endAngle = interpolate(t);


  return arcDef(d);


  };


  });





The Complete Sandbox Webpage


<!DOCTYPE html>


<html>


  <head>


  <meta http-equiv='X-UA-Compatible' content='IE=edge' />


  <title>Part 6</title>



  <div id='content'></div>


  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>


  <!-- <script src="file://d3/d3.js" charset="utf-8"></script>-->


  <script>


  var vis = d3.select("#content").append("svg:svg").attr("width", "100%").attr("height", "100%");



  var pi = Math.PI;



  //Viz definitiions


  var innerRad = 0;


  //var outerRad = 70;


  var width = 200;


  var height = 200;


  var startAngleDeg = -45;


  var endAngleDeg = 45;


  var colorCode = "red";




  //Outer Dimensions & Positioning


  var paddingTop = 10;


  var paddingBottom = 10;


  var paddingLeft = 10;


  var paddingRight = 10;



  //The total size of the component is calculated from its parts



  // Find the larger left/right padding


  var lrPadding = paddingLeft + paddingRight;


  var tbPadding = paddingTop + paddingBottom;


  var maxPadding = lrPadding;


  if (maxPadding < tbPadding){


  maxPadding = tbPadding


  }



  var outerRad = (width - 2*(maxPadding))/2;


  //var width = (outerRad * 2) + paddingLeft + paddingRight;


  //var height = (outerRad * 2) + paddingTop + paddingBottom;



  //The offset will determine where the center of the arc shall be


  var offsetLeft = outerRad + paddingLeft;


  var offsetDown = outerRad + paddingTop;



  //Don't let the arc have a negative length


  if (endAngleDeg < startAngleDeg){


  endAngleDeg = startAngleDeg;


  alert("End angle may not be less than start angle!");


  }




  var arcDef = d3.svg.arc()


  .innerRadius(innerRad)


  .outerRadius(outerRad);



  // Add the foreground arc in orange, currently showing 12.7%.


  var guageArc = vis.append("path")


     .datum({endAngle: startAngleDeg * (pi/180), startAngle: startAngleDeg * (pi/180)})


     .style("fill", "orange")


     .attr("transform", "translate(" + offsetLeft + "," + offsetDown + ")")


     .attr("d", arcDef);







  ///////////////////////////////////////////


  //Lets build a border ring around the gauge


  ///////////////////////////////////////////


  //var visRing = d3.select("#content").append("svg:svg").attr("width", "100%").attr("height", "100%");


  var ringThickness = 2;


  var ringOuterRad = outerRad + ringThickness;  //Outer ring starts at the outer radius of the inner arc


  var ringColorCode = "black";


  var ringStartAngleDeg = 0;


  var ringEndAngleDeg = 360;



  //Don't let the arc have a negative length


  if (ringEndAngleDeg < ringStartAngleDeg){


  ringEndAngleDeg = ringStartAngleDeg;


  alert("End angle of outer ring may not be less than start angle!");


  }


  var ringArcDefinition = d3.svg.arc()


  .innerRadius(outerRad)


  .outerRadius(ringOuterRad);



  var ringArc = vis.append("path")


  .datum({endAngle: ringEndAngleDeg * (pi/180), startAngle: ringStartAngleDeg * (pi/180), opacity: 0.0001})


  .attr("d", ringArcDefinition)


  .attr("fill", ringColorCode)


  .attr("transform", "translate(" + offsetLeft + "," + offsetDown + ")");





  ///////////////////////////////////////////


  //Lets build a the start and end lines


  ///////////////////////////////////////////


  var bracketThickness = 2;


  var lineData = [endPoints (outerRad, startAngleDeg), {x:offsetLeft, y:offsetDown}, endPoints (outerRad, endAngleDeg)];


  var visStartBracket = d3.select("#content").append("svg:svg").attr("width", "100%").attr("height", "100%");


  var lineFunction = d3.svg.line()


  .x(function(d) { return d.x; })


  .y(function(d) { return d.y; })


  .interpolate("linear");



  var borderLines = vis


  .attr("width", width).attr("height", height) // Added height and width so line is visible


  .append("path")


  .attr("stroke", ringColorCode)


  .attr("stroke-width", bracketThickness)


  .attr("fill", "none");



  //Helper function


  function endPoints (lineLength, lineAngle){


  var endX = offsetLeft - (lineLength * Math.sin(lineAngle * (pi/180)));


  var endY = offsetDown - (lineLength * Math.cos(lineAngle * (pi/180)));


  return {x:endX, y:endY}


  }



  ///////////////////////////////////////////


  //Lets add the indicator needle


  ///////////////////////////////////////////



  //needleWaypoints is defined with positive y axis being up


  var needleWaypoints = [{x: 0,y: 100}, {x: 10,y: 0}, {x: 0,y: -10}, {x: -10,y: 0}, {x: 0,y: 100}]



  //we need to invert the y-axis and scale the indicator to the gauge.


  //  If Y = 100, then that is 100% of outer radius.  So of Y = 100 and outerRad = 70, then the scaled Y will be 70.


  var needleFunction = d3.svg.line()


  .x(function(d) { return (d.x)*(outerRad/100); })


  .y(function(d) { return -1*(d.y)*(outerRad/100); })


  .interpolate("linear");



  var needle = vis


  .append("g")


      .attr("transform", "translate(" + offsetLeft + "," + offsetDown + ")")


  .append("path")


  .data(needleWaypoints)


      .attr("d", needleFunction(needleWaypoints))


  .attr("stroke", ringColorCode)


  .attr("stroke-width", bracketThickness)


  .attr("fill", ringColorCode)


  .attr("transform", "rotate(" + startAngleDeg + ")");







  ///////////////////////////////////////////


  //Lets add animations


  ///////////////////////////////////////////



  var delayArc = 500;


  var durationArc = 5000;


  var easeArc= "linear"; //https://github.com/mbostock/d3/wiki/Transitions#d3_ease




  //Arcs are in radians, but rotation transformations are in degrees.  Kudos to D3 for consistency


  needle.transition()


  .attr("transform", "rotate(" + endAngleDeg + ")")


  .duration(durationArc)


  .delay(delayArc)


  .ease(easeArc);




  //This blog post explains using attrTween for arcs: http://bl.ocks.org/mbostock/5100636


  // Function adapted from this example


  // Creates a tween on the specified transition's "d" attribute, transitioning


  // any selected arcs from their current angle to the specified new angle.


  guageArc.transition()


  .duration(durationArc)


        .attrTween("d", function(d) {


     var interpolate = d3.interpolate(d.endAngle, endAngleDeg * (pi/180));


     return function(t) {


  d.endAngle = interpolate(t);


  return arcDef(d);


  };


  });



  ringArc.transition()


  .attr( "fill-opacity", 0 )


  .transition()


  .delay( delayArc )


  .duration(durationArc)


            .attr( "fill-opacity", 1 );




  </script>


    </head>


  <body class='sapUiBody'>


  <div id='content'></div>


  </body>


</html>





This video shows the animations (with the exception of the ring fade) in action:

Next time, we'll bring the animations into the component.