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 looked at D3 transitions and used them to animate our gauge in the sandbox html file.  In this installment, we're going to bring those transitions into the component.  There are a few things that we want to keep in mind:

  • We'll include a new opacity setting in the properties sheet for the guide ring and guide lines.
  • As long as were introducing opacity, we'll also gratuitously add an opacity setting for the main gauge arc.
  • The guide ring and guide lines both look like drawn strokes same on screen, but tick a bit differently.  The guide lines are strokes.  The guide ring is an arc with the same width as the stroke.  This means that the guide ring will use fill-opacity to define its opacity, while the opacity of the guide lines will be defined by stroke-opacity.  The main arc's opacity is defined by fill-opacity.
  • We'll use the same delay and duration for the needle and arc.
  • We'll use a separate delay and duration for the opacity transition; which we'll call fade in the properties pane.
  • We will not animate the opacity of the main arc.  In D3, we can't run two transitions simultaneously on the same element.  We can chain them, but we can't run them in parallel.  This would mean fade first, then rotate, or rotate then fade; neither of which would be visible to the user.  If we wanted to, we could include logic for allowing both options and having the designer choose between either rotation or opacity fade, but we'll leave that out of the tutorial.  The main arc will have a rotation animation only.
  • We'll include boolean toggles for the rotation animation and the opacity fade.  In practice, setting them to false will set delay and duration to 0 milliseconds each.

Component.xml changes

Looking through the above bullet points, we have 9 new properties:

PropertyTypeDefault Value
gaugeOpacityfloat1.0
guideOpacityfloat1.0
animationEnablebooleanfalse
animationDelayint500
animationDurationint1000
animationEaseString"linear"
animationEnableOpacitybooleanfalse
animationDelayOpacityint500
animationDurationOpacityint500

The above properties translate into the following property elements:


<property


  id="gaugeOpacity"


  title="Opacity"


  type="float"


  group="SCNGaugeAngleSettings"/>


<property id="guideOpacity"


  title="Guide Opacity"


  type="float"


  group="SCNGaugeLineSettings"/>


<property id="animationEnable"


  title="Enable Animations"


  type="boolean"


  tooltip="Are the gauge arc and needle animated?"


  group="SCNGaugeAnimationSettings"/>


<property id="animationDelay"


  title="Animation Delay"


  type="int"


  tooltip="Delay time (in miliseconds), before the animation starts"


  group="SCNGaugeAnimationSettings"/>


<property id="animationDuration"


  title="Animation Duration"


  type="int"


  tooltip="Duration time (in miliseconds) of the animation.  Includes dampening oscillation time if ease type is elastic."


  group="SCNGaugeAnimationSettings"/>


<property id="animationEase"


  title="Ease Type"


  type="String"


  tooltip="Delay time (in miliseconds), before the animation starts"


  group="SCNGaugeAnimationSettings">


  <possibleValue>linear</possibleValue>


  <possibleValue>quad</possibleValue>


  <possibleValue>cubic</possibleValue>


  <possibleValue>sin</possibleValue>


  <possibleValue>exp</possibleValue>


  <possibleValue>circle</possibleValue>


  <possibleValue>elastic</possibleValue>


  <possibleValue>back</possibleValue>


  <possibleValue>bounce</possibleValue>


</property>


<property id="animationEnableOpacity"


  title="Enable Opacity Fade"


  type="boolean"


  tooltip="Enable Animated Opacity Fade?"


  group="SCNGaugeAnimationSettings"/>


<property id="animationDelayOpacity"


  title="Opacity Fade Delay"


  type="int"


  tooltip="Delay time (in miliseconds), before the opacity fade starts starts"


  group="SCNGaugeAnimationSettings"/>


<property id="animationDurationOpacity"


  title="Opacity Fade Duration"


  type="int"


  tooltip="Duration time (in miliseconds) of opacity fade."


  group="SCNGaugeAnimationSettings"/>



And initialization is also straightforward:


<initialization>


  ...


  <defaultValue property="gaugeOpacity">1.0</defaultValue>


  <defaultValue property="guideOpacity">1.0</defaultValue>


  <defaultValue property="animationEnable">false</defaultValue>


  <defaultValue property="animationDelay">500</defaultValue>


  <defaultValue property="animationDuration">1000</defaultValue>


  <defaultValue property="animationEase">linear</defaultValue>


  <defaultValue property="animationEnableOpacity">false</defaultValue>


  <defaultValue property="animationDelayOpacity">500</defaultValue>


  <defaultValue property="animationDurationOpacity">500</defaultValue>


</initialization>



Component.js

In the component.js file, we're going to do three things:

  • Include the usual client side delegate for all new properties, as well as the getter/setters needed to keep property values in sync between the server and client.
  • Up to now, the main arc, the needle and base pin have all been aligned with the end angle on creation.  We're now going to change this and align them with the start angle on creation.  In the arc, we'll do this by modifying the datum element, so that the startAngle and endAngle properties are the same.
  • All opacity fade and rotation animations will run all the time; with the opacity running from zero to the guide opacity setting and the rotation transition ending at the endAngle.  What this means concretely, is that if animations are disabled for a particular property (opacity or rotation), then the delay and duration both default to zero milliseconds for that transition.

We follow the usual conventions for the client side delegate for all properties:


me._gaugeOpacity = 1.0,


me._guideOpacity = 1.0;


me._animationEnable = false;


me._animationDelay = 500;


me._animationDuration = 1000;


me._animationEase = "linear";


me._animationEnableOpacity = false;


me._animationDelayOpacity = 500;


me._animationDurationOpacity = 500;



And the getter/setters:


me.gaugeOpacity = function(value) {


  if (value === undefined) {


  return me._gaugeOpacity;


  } else {


  me._gaugeOpacity = value;


  return me;


  }


};


me.guideOpacity = function(value) {


  if (value === undefined) {


  return me._guideOpacity;


  } else {


  me._guideOpacity = value;


  return this;


  }


};


me.animationEnable = function(value) {


  if (value === undefined) {


  return me._animationEnable;


  } else {


  me._animationEnable = value;


  return me;


  }


};


me.animationDelay = function(value) {


  if (value === undefined) {


  return me._animationDelay;


  } else {


  me._animationDelay = value;


  return me;


  }


};


me.animationDuration = function(value) {


  if (value === undefined) {


  return me._animationDuration;


  } else {


  me._animationDuration = value;


  return me;


  }


};


me.animationEase = function(value) {


  if (value === undefined) {


  return me._animationEase;


  } else {


  me._animationEase = value;


  return me;


  }


};


me.animationEnableOpacity = function(value) {


  if (value === undefined) {


  return me._animationEnableOpacity;


  } else {


  me._animationEnableOpacity = value;


  return me;


  }


};


me.animationDelayOpacity = function(value) {


  if (value === undefined) {


  return me._animationDelayOpacity;


  } else {


  me._animationDelayOpacity = value;


  return me;


  }


};


me.animationDurationOpacity = function(value) {


  if (value === undefined) {


  return me._animationDurationOpacity;


  } else {


  me._animationDurationOpacity = value;


  return me;


  }


};



The new gauge arc, with opacity and the new starting data:


if (me._enableArc == true){


  var arcDef = d3.svg.arc()


  .innerRadius(me._innerRad)


  .outerRadius(me._outerRad);




  var guageArc = vis.append("path")


  .datum({endAngle: me._endAngleDeg * (pi/180), startAngle: me._startAngleDeg * (pi/180)})


     .style("fill", me._displayedColor)


     .attr("width", me.$().width()).attr("height", me.$().height()) // Added height and width so arc is visible


     .attr("transform", "translate(" + me._offsetLeft + "," + me._offsetDown + ")")


     .attr("d", arcDef)


     .attr( "fill-opacity", me._gaugeOpacity );


}



The reworked indicator needle and base pin:


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


//Lets add the indicator needle


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




if (me._enableIndicatorNeedle == true){


  var needleWaypointOffset = me._needleWidth/2;




  //needleWaypoints is defined with positive y axis being up


  //The initial definition of needleWaypoints is for a full diamond, but if me._enableIndicatorNeedleTail is false, we'll abbreviate to a chevron


  var needleWaypoints = [{x: 0,y: me._needleHeadLength}, {x: needleWaypointOffset,y: 0}, {x: 0,y: (-1*me._needleTailLength)}, {x: (-1*needleWaypointOffset),y: 0}, {x: 0,y: me._needleHeadLength}]


  if (me._enableIndicatorNeedleTail == false){


  if (me._fillNeedle == false){


  //If we have no tail and no fill then there is no need to close the shape.


  //Leave it as an open chevron


  needleWaypoints = [{x: needleWaypointOffset,y: 0}, {x: 0,y: me._needleHeadLength}, {x: (-1*needleWaypointOffset),y: 0}];


  }


  else {


  //There is no tail, but we are filling the needle.


  //In this case, draw it as a triangle


  needleWaypoints = [{x: 0,y: me._needleHeadLength}, {x: needleWaypointOffset,y: 0}, {x: (-1*needleWaypointOffset),y: 0}, {x: 0,y: me._needleHeadLength}]


  }




  }




  //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)*(me._outerRad/100); })


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


  .interpolate("linear");




  //Draw the needle, either filling it in, or not


  var needleFillColorCode = me._needleColorCode;


  if (me._fillNeedle == false){


  needleFillColorCode = "none";


  }





  var needle = vis


  .append("g")


  .attr("transform", "translate(" + me._offsetLeft + "," + me._offsetDown + ")")


  .append("path")


  .datum(needleWaypoints)


  .attr("class", "tri")


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


  .attr("stroke", me._needleColorCode)


  .attr("stroke-width", me._needleLineThickness)


  .attr("fill", needleFillColorCode)


  .attr("transform", "rotate(" +  me._startAngleDeg + ")");;




}






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


//Lets add a needle base pin


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






if (me._enableIndicatorNeedleBase == true){


  // Like the rest of the needle, the size of the pin is defined relative to the main arc, as a % value


  var needleIBasennerRadius = (me._needleBaseWidth/2)*(me._outerRad/100) - (me._needleLineThickness/2);


  var needleBaseOuterRadius = needleIBasennerRadius + me._needleLineThickness;


  if (me._fillNeedlaBasePin == true){


  needleIBasennerRadius = 0.0;


  }





  // The pin will either be a 180 degree arc, or a 360 degree ring; starting from the 9 O'clock position.


  var needleBaseStartAngle = 90.0;


  var needleBaseEndAngle = 270.0;


  if (me._fullBasePinRing == true){


  needleBaseEndAngle = 450.0;


  }




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


  if (needleBaseEndAngle < needleBaseStartAngle){


  needleBaseEndAngle = needleBaseStartAngle;


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


  }




  //Transfomation for the Pin Ring


  // We won't apply it just yet


  var nbpTransformedStartAngle = needleBaseStartAngle + me._startAngleDeg;


  var nbpTransformedEndAngle = needleBaseEndAngle + me._startAngleDeg;



  var nbTransformedStartAngle = needleBaseStartAngle + me._endAngleDeg;


  var nbTransformedEndAngle = needleBaseEndAngle + me._endAngleDeg;




  var pinArcDefinition = d3.svg.arc()


  .innerRadius(needleIBasennerRadius)


  .outerRadius(needleBaseOuterRadius);




  var pinArc = vis.append("path")


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


  .attr("d", pinArcDefinition)


  .attr("fill", me._needleColorCode)


  .attr("transform", "translate(" + me._offsetLeft + "," + me._offsetDown + ")");


}



Transition Duration and Timing

Somewhere in the me.redraw() function, prior to the transitions, we need to check to see whether or not animations have been enabled and set the timing to 0 if required:


//Prepare the animation settings


// If me._animationEnable is false, then we'll act as if me._animationDelay and me._animationDuration


//   are both 0, without actually altering their values.


var tempAnimationDelay = 0;


var tempAnimationDuration = 0;


if (me._animationEnable == true){


  tempAnimationDelay = me._animationDelay;


  tempAnimationDuration = me._animationDuration;


}




//Guide Ring and Lines


var localFadeDelay = me._animationDelayOpacity;


var localFadeDuration = me._animationDurationOpacity;


if (me._animationEnableOpacity == false){


  localFadeDelay = 0;


  localFadeDuration = 0;


}



Let's Add our Animations

We'll pretty much just leave the sandbox transition code untouched, except for refactoring it into the component:


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


//Lets add our animations


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


//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.


if (me._enableArc == true){


  guageArc.transition()


  .duration(tempAnimationDuration)


  .delay(tempAnimationDelay)


  .ease(me._animationEase)


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


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


     return function(t) {


      d.endAngle = interpolate(t);


  return arcDef(d);


  };


  });


}




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


if (me._enableIndicatorNeedle == true){


  needle.transition()


  .attr("transform", "rotate(" + me._endAngleDeg + ")")


  .duration(tempAnimationDuration)


  .delay(tempAnimationDelay)


  .ease(me._animationEase);


}


if (me._enableIndicatorNeedleBase == true){


  pinArc.transition()


  .duration(tempAnimationDuration)


  .delay(tempAnimationDelay)


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


     var interpolateEnd = d3.interpolate(nbpTransformedEndAngle * (pi/180), nbTransformedEndAngle * (pi/180));


     var interpolateStart = d3.interpolate(nbpTransformedStartAngle * (pi/180), nbTransformedStartAngle * (pi/180));


     return function(t) {


      d.endAngle = interpolateEnd(t);


      d.startAngle = interpolateStart(t);


  return pinArcDefinition(d);


  };


  });



}




//Guide Ring and Lines


if (me._enableGuideRing == true){


  ringArc.transition()


  .attr( "fill-opacity", 0 )


  .transition()


  .delay( localFadeDelay )


  .duration(localFadeDuration)


        .attr( "fill-opacity", me._guideOpacity );


}


if (me._enableGuideLines == true){


  borderLines.transition()


  .attr( "stroke-opacity", 0 )


  .transition()


  .delay( localFadeDelay )


  .duration(localFadeDuration)


        .attr( "stroke-opacity", me._guideOpacity );


}



Result

Note that I created the video before adding the opacity fade.

We now have animated gauge angles and guide opacity.  As usual, the current version of the project is available in a project on Github.

Next time, we'll begin looking at adding dynamic text callouts.