Source: Piechart.js

/**
 * Piechart creates a new Piechart
 * @version 1.0
 * @author Jonas Marcello <rujmarcello@gmail.com>
 * @instance
 * @module EsbmePlots
 * @memberof module:EsbmePlots
 * @augments module:EsbmePlots
 * @constructor module:EsbmePlots.Piechart
 * @example
 * var x = Piechart()
 *  .parent(d3.selectAll(".container"))
 *  .categories(["Boys","Girls"])
 *  .seriesNames(["blue","pink"])
 *  .margin("auto")
 *  .data([[30,26])
 *  .draw()
 */
function Piechart() {
  var categories = [],
    showValues  = "number",
    showCategories = true,
    innerRadius = 0,
    cornerRadius = 0,
    total = 0


  // creates a new EsbmePlot instance to use as Piechart
  var plot = new EsbmePlot()

  // updates the default classname to still include the id, but
  // also contain "piechart"
  var id = plot.className()
  plot.className("piechart "+id)

  /**
   * <p>Defines the category names to be printed on the x-axis</p>
   * If value is specified, sets the categories.<br>
   * If value is not specified, return the current categories
   * @memberof module:EsbmePlots.Piechart
   * @instance
   * @param {String[]} [value]
   * @method categories
   */
  plot.categories = function(_){
    if (arguments.length){
      categories = _
      return plot
    } else {
      return categories
    }
  }

  /**
   * <p>Defines if percentage, numbers or nothing should be written on each pie</p>
   * If value is specified, sets showValues.<br>
   * If value is not specified, return the current value
   * @memberof module:EsbmePlots.Piechart
   * @instance
   * @param {String} [value="number"] - Can be either "number", "percent" or null
   * @method showValues
   */
  plot.showValues = function(_){
    if (arguments.length){
      showValues = _
      return plot
    } else {
      return showValues
    }
  }

  /**
   * <p>Defines whether the names of the category should be written on each pie</p>
   * If value is specified, sets the value.<br>
   * If value is not specified, returns the current value
   * @memberof module:EsbmePlots.Piechart
   * @instance
   * @param {String[]} [value]
   * @method showCategories
   */
  plot.showCategories = function(_){
    if (arguments.length){
      showCategories = _
      return plot
    } else {
      return showCategories
    }
  }

  /**
   * <p>Defines the inner radius of the arcs to create a donut-chart</p>
   * If value is specified, sets the value.<br>
   * If value is not specified, returns the current value
   * @memberof module:EsbmePlots.Piechart
   * @instance
   * @param {Integer} [value=0]
   * @method innerRadius
   */
  plot.innerRadius = function(_){
    if (arguments.length){
      innerRadius = _
      return plot
    } else {
      return innerRadius
    }
  }
  /**
   * <p>Defines the corner radius of each pie</p>
   * If value is specified, sets the value.<br>
   * If value is not specified, returns the current value
   * @memberof module:EsbmePlots.Piechart
   * @instance
   * @param {Integer} [value]
   * @method cornerRadius
   */
  plot.cornerRadius = function(_){
    if (arguments.length){
      cornerRadius = _
      return plot
    } else {
      return cornerRadius
    }
  }

  plot.checkData = checkData

  plot.drawData = drawData

  return plot

  /**
   * <p>Draws the piechart</p>
   * @memberof module:EsbmePlots.Piechart
   * @instance
   * @param  {D3Selection} canvas
   * @param  {Object} dimensions  - Dimensions of the plot (margins etc)
   */
  function drawData(canvas,dimensions){
    var radius = Math.floor(
      Math.min(dimensions.plotWidth,dimensions.plotHeight)/2
    )
    if (innerRadius >= radius){
      throw new TypeError("Inner radius must be smaller than the outer radius")
    }
    var className = plot.className()
    var seriesNames = plot.seriesNames()
    var infos = plot.info()

    var pieData = d3.pie()
      .sortValues(null)(plot.data())

    // Add normalized properties to each piece
    // for mousever events etc
    pieData.forEach(function(pieceData,i){
      pieceData.category = categories[i]
      pieceData.seriesName = seriesNames[i]
      pieceData.total = total,
      pieceData.info = infos[i]
    })
    var arc = d3.arc()
      .outerRadius(radius)
      .innerRadius(innerRadius)
      .cornerRadius(cornerRadius)

    var center = canvas.selectAll("g.center").data([pieData])
    center.exit().remove()
    center = center.enter()
      .append("g")
      .merge(center)
      .attr("class","esbmeplot center "+className)
      .attr("transform","translate("+dimensions.plotWidth/2+","+radius+")")

    // Creates an array with data from the last draw() event
    // Those ones are used as starting point for transitions
    var oldAngles = center.selectAll("path.piece").data()
      .map(function(item){
        return {
          startAngle: item.startAngle,
          endAngle: item.endAngle
        }
      })
    // In case there is more data than previous pieces
    // this add fake previous 360° angles to all new elements
    pieData.slice(oldAngles.length).forEach(function(){
      oldAngles.push({
        startAngle: 2*Math.PI,
        endAngle: 2*Math.PI
      })
    })

    var pieces = center.selectAll("path.piece").data(pieData)

    pieces.exit().remove()
    pieces = pieces.enter()
      .append("path")
      // new pieces get 360° angles as starting point
      .attr("d",function(){
        return arc({startAngle:2*Math.PI,endAngle:2*Math.PI})
      })
      .merge(pieces)
      .attr("class",function(d){
        return "esbmeplot piece "+d.seriesName + " "+ className
      })

    pieces.transition().duration(plot.duration())
      .attrTween("d", function(d,i){
        return arcTween(d,i)
      })

    var categoryLabels = center.selectAll("text.category-label")
      .data(pieData)
    categoryLabels.exit().remove()
    if (showCategories) {
      categoryLabels = categoryLabels.enter()
        .append("text")
        .merge(categoryLabels)
        .attr("class","esbmeplot category-label "+className)
        .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")" })
        .attr("dy", "-0.5em")
        .attr("text-anchor", "middle")
        .style("cursor","default")
        .text(function(d){return d.category})
    } else {
      categoryLabels.remove()
    }

    var valueLabels = center.selectAll("text.value-label")
      .data(pieData)
    valueLabels.exit().remove()
    if (showValues) {
      valueLabels = valueLabels.enter()
        .append("text")
        .merge(valueLabels)
        .attr("class","esbmeplot value-label "+className)
        .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")" })
        .attr("dy", "0.5em")
        .attr("text-anchor", "middle")
        .style("cursor","default")
        .text(function(d){
          return showValues === "percent" ?
            Math.floor( d.value/d.total*100 ) + "%" :
            d.value
        })
    } else {
      valueLabels.remove()
    }

    plot.addEvents(pieces)
    plot.addEvents(categoryLabels)
    plot.addEvents(valueLabels)

    // Interpolation function for starting and end angles
    // of each piece
    function arcTween(d,i){
      var oldStart = oldAngles[i].startAngle
      var oldEnd = oldAngles[i].endAngle
      var intStart = d3.interpolate(oldStart, d.startAngle)
      var intEnd = d3.interpolate(oldEnd, d.endAngle)
      return function(t){
        return arc({
          startAngle: intStart(t),
          endAngle: intEnd(t)
        })
      }
    }

    return plot
  }

  function checkData(){
    if (!(plot.data() instanceof Array)){
      throw new TypeError("Piechart expects an Array as data")
    }
    total = plot.data().reduce(function(total,datum){
      if (typeof datum !== "number"){
        throw new TypeError("Values given to Piechart must be numberic")
      }
      return total+datum
    })
    if (total === 0) {
      plot.data([1])
      categories = ["No data available"]
      plot.seriesNames(["invalid"])
      plot.events({})
      showValues = false
      showCategories = true
    }
  }
}