<link href="//cdn.muicss.com/mui-0.5.1/css/mui.min.css" rel="stylesheet" type="text/css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/es6-promise/3.2.1/es6-promise.min.js"></script>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//cdn.muicss.com/mui-0.5.1/js/mui.min.js"></script>

<script>
/**
 * @constructor DashTimer
 * uses D3 to plot circular timer
 * @param {object|string} div the container for the viz an element or #name
 */
function DashTimer(div) {
  var TAU = 2 * Math.PI,
    NAME = "DashTimer";

  var self = this,
    data_, dash_,
    dInfo_ = {},
    div_ = Object(div) === div ? '#' + div.id : div;

  /**
   * initialize the arc data
   * @param {[object]} data the array of data for each arc
   * @param {object} [options=] the viz options
   * @return {DashTimer} self
   */
  self.setData = function(data, options) {
    // first time in, set up the containers
    if (!dash_) {
      self.init(options)
    }
    // set up values for d3
    data_ = (data || [{}, {}]).map(function(d, i) {
      var m = vanMerge_([dash_, d]);
      m.index = i;
      m.startAngle = m.start.angle * TAU;
      m.endAngle = m.finish.angle * TAU;
      m.start.innerRadius = dash_.height / 2 * m.start.innerRatio;
      m.start.outerRadius = dash_.height / 2 * m.start.outerRatio;
      m.innerRadius = m.start.innerRadius;
      m.outerRadius = m.start.outerRadius;
      m.finish.innerRadius = dash_.height / 2 * m.finish.innerRatio;
      m.finish.outerRadius = dash_.height / 2 * m.finish.outerRatio;
      m.value = m.start.value;
      m.static = {
        endAngle: m.immediate.angle,
        innerRadius: m.finish.innerRadius === m.start.innerRadius,
        outerRadius: m.finish.outerRadius === m.start.outerRadius,
        value: m.start.value === m.finish.value
      };
      m.dataName = m.dataName || NAME + i;
      return m;
    });
    d3.select(div_).html("");
    dInfo_.arc = d3.svg.arc();
    dInfo_.svg = d3.select(div_).append("svg")
      .attr("width", dash_.width)
      .attr("height", dash_.height)
      .append("g")
      .attr("transform", "translate(" +
        dash_.width / 2 + "," + dash_.height / 2 + ")");


    dInfo_.textElements = dInfo_.svg.append("text");
    (dash_.values.styles.split(";") || []).forEach(function(d) {
      if (d) {
        var s = d.split(":");
        if (s.length !== 2) throw 'invalid style ' + d;
        dInfo_.textElements.style(s[0], s[1]);
      }
    });

    if (dash_.values.classes) {
      dash_.values.classes.split(" ").forEach(function(d) {
        if (d) dInfo_.textElements.classed(d, true);
      });
    }
    dash_.internal.cancelled = dash_.internal.paused= dash_.internal.finished= false;

    resetPromise_();
    
    return self;
  };

  /**
   * initialize the arcs
   * @param {object} options the viz options
   * @return {DashTimer} self
   */
  self.init = function(options) {

    // merge default settings with given
    dash_ = vanMerge_([{     // default settings - many can be overridden in data for individual items
      height: 100,           // height of viz
      width: 100,            // width of viz (since a circle both are equal)
      ease: "linear",        // type of easing (pause/resume will only work reliably with linear)
      duration: 5000,        // tranistion duration
      callback:"",           // function (d, self) to call back on every transition step
      name: NAME + new Date().getTime(), // name of dashtimer
      start: {               // values at start of transition
        innerRatio: .8,      // diameter of inner circle relative to height
        outerRatio: .95,      // diameter of outer circle relative to height
        angle: 0,            // angle between 0 and 1
        fill: '#2196F3',     // fill color
        value: 0             // value to interpolate
      },
      finish: {              // values at end of transition
        innerRatio: .8,      // diameter of inner circle relative to height
        outerRatio: .95,     // diameter of outer circle relative to height
        angle: 1,            // angle between 0 and 1
        fill: '#FFC107',     // fill color
        value: 100           // value to interpolate
      },
      immediate: {           // if true then transition is skipped
        angle:false,         // the angle
      },
      values: {              // things to do with displaying values 
        classes: "",         // classes to apply separated by spaces
        styles: "text-anchor:middle;", // styles to apply separated by ;
        show: false,         // whether to show values
        decorate: function(d) {  // function to clean up data before displaying
          return Math.round(d); 
        }
      },           
      custom:{},             // should be used for any custom data to be carried around
      internal:{
        paused:false,
        progress:0,
        finished:true
      }                 
    }, options || {}]);

    return self;
  };
  /**
   * causes the timer to resolve from outside
   * @return {DashTimer} self
   */
  self.resolve= function() {
    dash_.internal.paused = false;
    dash_.internal.finished = true;
    dash_.mp.resolve(self);
    return self;
  };
  /**
   * causes the timer to reject from outside
   * @return {DashTimer} self
   */
  self.reject = function() {
    dash_.internal.paused = false;
    dash_.internal.finished = true;
    dash_.mp.reject(self);
    return self;
  };
  
  /**
   * pauses the timer right now
   * timeout promise is not resolved or rejected
   * @return {DashTimer} self
   */
  self.pause = function() {
    if (self.isRunning()) {
      dash_.internal.paused = true;
      dInfo_.path.transition(dash_.name).duration(0);
    }
    return self;
  };
  
  /**
   * resumes at the last place paused at
   * if not paused, nothing happens
   * @return {Promise | null} a promise to th resumed run - not normally required
   */
  self.resume = function() {
    // this will return the master promise
    if (self.isPaused()) {
      return work_(
        dash_.internal.progress,
        dash_.internal.duration * (dash_.finishAt - dash_.internal.progress),
        dash_.finishAt
      );
    }
  };
     
  /**
  * kills any ongoing transition
  * does not fire any promises
  * @return {DashTimer} self
  */
  self.cancel = function() {
    
    //if there is oe on the go then kill it and reject the promise
    if (dash_.mp) {
      dash_.internal.cancelled = true;
      // this will cause a zero transition to cancel the current one
      dInfo_.path.transition(dash_.name).duration(0);
      self.reject();
    }

    
  }
   /**
   * start a new timer
   * @param {number} duration number of seconds to run he transition for
   * @param {[object]} [data=] replace the current data with this
   * @param {number} [start=0] start place
   * @param {number} [finish=1] finish place
   * @return {Promise} a promise that'll be resolved when it times out
   */
  self.start = function(duration,start,finish) {
    resetPromise_();
    dash_.internal.duration = fixDef_ (duration,dash_.duration);
    return work_(fixDef_ (start,0) , dash_.internal.duration, fixDef_ (finish,1));
  };
  
  /**
   * get the current data 
   * @return {[object]} the data
   */
  self.getData = function() {
    return data_;
  };
  /**
   * get the current data 
   * @param {string} dataName the data name
   * @return {object} the data
   */
  self.getItem = function(dataName) {
    return data_.reduce(function(p,c) {
      return c.dataName === dataName ? c : p;
    },null);
  };
  /**
   * get all the current control values
   * @return {object} the current control values
   */
  self.getControl = function() {
    return dash_;
  };
  /**
   * get all the current control values
   * @return {object} the current control values
   */
  self.getVizInfo = function() {
    return dInfo_;
  };
  /**
   * is the viz currenly paused
   * @return {boolean} is it paused
   */
  self.isPaused = function() {
    return dash_.internal.paused;
  };
  /**
   * is the viz currenly finished
   * @return {boolean} is it finished
   */
  self.isFinished = function() {
    return dash_.internal.finished;
  };
  /**
   * is the viz currenly running
   * @return {boolean} is it running
   */
  self.isRunning = function() {
    return !self.isFinished() && !self.isPaused();
  };
  /**
   * is the viz currenly cancelled
   * @return {boolean} is it running
   */
  self.isCancelled = function() {
    return dash_.internal.cancelled;
  };
  /**
   * get the current progress
   * @return {number} between 0 and 1
   */
  self.getProgress = function() {
    return dash_.internal.progress;
  };
  /**
   * set the progress immediately and stop the viz
   * the completion promise is rejected
   * @param {number} between 0 and 1 to set it to
   * @param {number} [duration=0] how long to take to do it
   * @return {Promise} the master promise
   */
  self.setProgress = function(progress, duration) {
    return work_(0, fixDef_ (duration ,0 ), progress);
  };
  /**
   * reset the paths for the arcs
   */
  function redoPath_() {
    // redo the path
    dInfo_.path = dInfo_.svg.selectAll("path").data(data_);
    // add the new data
    dInfo_.enter = dInfo_.path.enter().append("path")
      .attr("fill", function(d) {
        return d.start.fill
      })
      .attr("d", dInfo_.arc);
    // get rid of any old
    dInfo_.path.exit().remove();
  }
  /**
   * make  a new master promise
   * @return {DashTimer} self
   */
  function resetPromise_() {

    dash_.mp = {};
    dash_.mp.promise = new Promise(function(resolve, reject) {
      dash_.mp.reject = reject;
      dash_.mp.resolve = resolve;
    });
    return self;
    
  }
  /**
   * co-ordinate the work, and manage the completion promises
   * @param {number} start place to start at between 0 and 1
   * @param {number} duration number of seconds to run he transition for
   * @param {number} [finish=1] place to stop at between 0 and 1
   * @return {Promise} a promise that'll be resolved when it times out
   */
  function work_(start, duration, finish) {

    // tidy up from last time
    redoPath_();
    finish = fixDef_ ( finish,1);

    // we'll need this if resuming
    dash_.finishAt = finish;
    dash_.startAt = start;

    // this returns a promise that simply resolves the master promise
    show_(start, duration, finish)
      .then(
        function(expired) {
          self.resolve();
        },
        function(stopped) {
          // nothing to do here - we dont want to resolve or reject the master promise
          // because this was just a pause
        });

    // return the master promise
    return dash_.mp.promise;
  }
 
  /**
   * do the transition
   * @param {number} startAt place to start at between 0 and 1
   * @param {number} duration number of seconds to run he transition for
   * @param {number} finishAt place to stop at between 0 and 1
   * @return {Promise} a promise that'll be resolved when it times out
   */
  function show_(startAt, duration, finishAt) {
    dash_.internal.paused = dash_.internal.finished = dash_.internal.cancelled = false, dash_.internal.progress = 0;
    // this is a promise that will be resolved when the timer runs out
    return new Promise(function(resolve, reject) {

      // interpolation include any startat/finishat generated by a pause
      // this returns a closure with the modified interpolation function
      function rng(a, b) {
        return d3.interpolate(
          d3.interpolate(a, b)(startAt), d3.interpolate(a, b)(1)
        );
      }

      dInfo_.path.transition(dash_.name)
        .ease(dash_.ease)
        .duration(duration)
        .each("end", function(d, i) {
          if (i === data_.length - 1) {
            dash_.internal.finished = true;
            resolve(self)
          }
        })
        .attrTween("d", function(d) {

          // these interpolate closures for each of the transitioing values
          d.interpolate = {
            innerRadius: rng(d.start.innerRadius, d.finish.innerRadius),
            outerRadius: rng(d.start.outerRadius, d.finish.outerRadius),
            endAngle: rng(d.start.angle * TAU, d.finish.angle * TAU),
            value: rng(d.start.value, d.finish.value)
          };

          // returns a closure for each tick
          return function(t) {
            // if a puase has been called in the meantime
            // a transition pause is signalled by rejecting the promise
            if (self.isPaused()) reject(self);

            // statics mean that they dont transition
            Object.keys(d.interpolate).forEach(function(k) {
              if (!d.static[k]) d[k] = d.interpolate[k](t * finishAt);
            });
            dash_.internal.progress = (startAt + (1 - startAt) * t * finishAt);
                      
            // callback if requested
            if(d.callback) d.callback(d , self , t);
            
            // show the interpolated value 
            if (d.values.show) {
              dInfo_.textElements.text(d.values.decorate(d.value));
            }
            return dInfo_.arc(d);
          };
        })
        .styleTween("fill", function(d) {
          return d3.interpolate(
            d3.interpolate(d.start.fill, d.finish.fill)(startAt),
            d3.interpolate(d.start.fill, d.finish.fill)(finishAt));
        });
    });
  };
  /**
   * recursively extend an object with other objects
   * @param {[object]} obs the array of objects to be merged
   * @return {object} the extended object
   */
  function vanMerge_(obs) {
    return (obs || []).reduce(function(p, c) {
      return vanExtend_(p, c);
    }, {});
  }

  function vanExtend_(result, opt) {

    result = result || {};
    return Object.keys(opt).reduce(function(p, c) {
      // if its an object
      if (typeof opt[c] === "object") {
        p[c] = vanExtend_(p[c], opt[c]);
      } else {
        p[c] = opt[c];
      }
      return p;
    }, result);
  }
  
  function fixDef_ (value , defValue) {
    return typeof value === typeof undefined ? defValue : value;
  }
};



</script>
<div class="mui-panel mui--text-center">
  <div id="clock">
  </div>
</div>
<div id="result"></div>
<button id="start">Start</button>
<button id="pause">Pause</button>
<button id="resume">Resume</button>
<button id="cancel">Cancel</button>
function getClockData() {
  var now = new Date();
  return [{
      dataName: 'hoursShadow',
      start: {
        fill: '#B3E5FC',
        outerRatio: .7,
        innerRatio: .55,
        angle: 0
      },
      finish: {
        fill: '#B3E5FC',
        outerRatio: .7,
        innerRatio: .55,
        angle: now.getHours() / 24
      },
      immediate: {
        angle: true
      }
    }, {
      dataName: 'minutesShadow',
      start: {
        fill: '#F0F4C3',
        outerRatio: .85,
        innerRatio: .7,
        angle: 0
      },
      finish: {
        fill: '#F0F4C3',
        outerRatio: .85,
        innerRatio: .7,
        angle: now.getMinutes() / 60
      },
      immediate: {
        angle: true
      }
    }, {
      dataName: "hours",
      start: {
        angle: now.getHours() / 24,
        outerRatio: .7,
        innerRatio: .55,
        fill: '#B3E5FC'
      },
      finish: {
        angle: 1/60/24 + (now.getHours() / 24),
        outerRatio: .7,
        innerRatio: .55,
        fill: '#B3E5FC'
      }
    }, {
      dataName: "minutes",
      start: {
        angle: now.getMinutes() / 60,
        outerRatio: .85,
        innerRatio: .7,
        fill: '#F0F4C3'
      },
      finish: {
        angle: 1/60 + (now.getMinutes() / 60),
        outerRatio: .85,
        innerRatio: .7,
        fill: '#F0F4C3'
      }
    }, {
      dataName: "seconds",
      start: {
        angle: now.getSeconds() / 60,
        outerRatio: 2,
        innerRatio: .85,
        fill: '#E1BEE7'
      },
      finish: {
        angle: 1 + (now.getSeconds() / 60),
        outerRatio: 2,
        innerRatio: .85,
        fill: '#FFEB3B'
      },
      values: {
        show: true
      },
    }

  ];
}

var clockOptions = {
  duration: 60 * 1000,
  values: {
    classes: "mu--text-caption",
    styles: "font-size:.7em;text-anchor:middle;",
    decorate: function(d) {
      return d3.time.format("%X")(new Date());
    }
  }
};

var timer = new DashTimer('#clock').init(clockOptions);
endless();
d3.select('#cancel').on("click", function() {
  timer.cancel();
});
d3.select('#pause').on("click", function() {
  timer.pause();
});
d3.select('#resume').on("click", function() {
  timer.resume();
});
d3.select('#start').on("click", function() {
  endless();
});

function endless() {
  timer.setData(getClockData());
  timer.start()
    .then(
      function(expired) {
        endless();
      },
      function(cancelled) {
        d3.select('#result').text('cancelled');
      }
    )
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.