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:

Property Type Default Value
gaugeOpacity float 1.0
guideOpacity float 1.0
animationEnable boolean false
animationDelay int 500
animationDuration int 1000
animationEase String “linear”
animationEnableOpacity boolean false
animationDelayOpacity int 500
animationDurationOpacity int 500

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.

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply