class @Refresher τ = 2 * Math.PI # http://tauday.com/tau-manifesto constructor: -> @_rate = 1000 # 1 second @_interval = 5 * 60 * 1000 # 5 minutes @width = 42 @height = @width @innerRadius = @width / 3 @outerRadius = @width / 2 @_container = 'body' rate: (@_rate)-> @ container: (@_container)-> @ interval: (@_interval)-> @ callback: (@_callback)-> @ render: -> # An arc function with all values bound except the endAngle. So, to compute an # SVG path string for a given angle, we pass an object with an endAngle # property to the `arc` function, and it will return the corresponding string. @arc = d3.svg.arc() .innerRadius(@innerRadius) .outerRadius(@outerRadius) .startAngle(0) # Create the SVG container, and apply a transform such that the origin is the # center of the canvas. This way, we don't need to position arcs individually. svg = d3.select(@_container).append('svg') .attr('width', @width) .attr('height', @height) .attr('class', 'refresher') .append('g') .attr('transform', "translate(#{@width / 2},#{@height / 2})") # Add the background arc, from 0 to 100% (τ). background = svg.append('path') .datum(endAngle: τ) .attr('class', 'refresher-track') .attr('d', @arc) # Add the foreground arc, currently showing 0%. @foreground = svg.append('path') .datum(endAngle: 0) .attr('class', 'refresher-path') .attr('d', @arc) @tween = _.bind(@arcTween, @) setInterval(_.bind(@tick, @), @_rate) @start() start: -> @_startTime = +(new Date()) @_endTime = @_startTime + @_interval @foreground .datum(endAngle: 0) .attr('d', @arc) .transition() .duration(@_rate) .ease('linear') .call(@tween, (@_rate / @_interval) * τ) tick: -> time = +(new Date()) if time > @_endTime @_callback() if @_callback @start() else percent = (time + @_rate - @_startTime) / @_interval @foreground.transition() .duration(@_rate) .ease('linear') .call(@tween, percent * τ) # Creates a tween on the specified transition's 'd' attribute, transitioning # any selected arcs from their current angle to the specified new angle. arcTween: (transition, newAngle)-> # The function passed to attrTween is invoked for each selected element when # the transition starts, and for each element returns the interpolator to use # over the course of transition. This function is thus responsible for # determining the starting angle of the transition (which is pulled from the # element's bound datum, d.endAngle), and the ending angle (simply the # newAngle argument to the enclosing function). transition.attrTween 'd', (d)=> # To interpolate between the two angles, we use the default d3.interpolate. # (Internally, this maps to d3.interpolateNumber, since both of the # arguments to d3.interpolate are numbers.) The returned function takes a # single argument t and returns a number between the starting angle and the # ending angle. When t = 0, it returns d.endAngle when t = 1, it returns # newAngle and for 0 < t < 1 it returns an angle in-between. interpolate = d3.interpolate(d.endAngle, newAngle) # The return value of the attrTween is also a function: the function that # we want to run for each tick of the transition. Because we used # attrTween('d'), the return value of this last function will be set to the # 'd' attribute at every tick. (It's also possible to use transition.tween # to run arbitrary code for every tick, say if you want to set multiple # attributes from a single function.) The argument t ranges from 0, at the # start of the transition, to 1, at the end. (t)=> # Calculate the current arc angle based on the transition time, t. Since # the t for the transition and the t for the interpolate both range from # 0 to 1, we can pass t directly to the interpolator. # Note that the interpolated angle is written into the element's bound # data object! This is important: it means that if the transition were # interrupted, the data bound to the element would still be consistent # with its appearance. Whenever we start a new arc transition, the # correct starting angle can be inferred from the data. d.endAngle = interpolate(t) # Lastly, compute the arc path given the updated data! In effect, this # transition uses data-space interpolation: the data is interpolated # (that is, the end angle) rather than the path string itself. # Interpolating the angles in polar coordinates, rather than the raw path # string, produces valid intermediate arcs during the transition. @arc(d)