Source: Barplot.js


/**
 * Barplot creates a new Barplot
 * @version 1.0
 * @author Jonas Marcello <rujmarcello@gmail.com>
 * @instance
 * @module EsbmePlots
 * @memberof module:EsbmePlots
 * @augments module:EsbmePlots
 * @constructor module:EsbmePlots.Barplot
 * @example
 * var x = Barplot()
 *  .parent(d3.selectAll(".container"))
 *  .categories(["Monday","Tuesday","Wednesday"])
 *  .margin("auto")
 *  .xLabelRotation(-45)
 *  .data([[3,1],[4,5],[6,7]])
 *  .yLabel("# of commits")
 *  .xLabel("Weekday")
 *  .draw()
 */
function Barplot(){
  var stacked = true,
    categories = [],
    xLabelRotation = 0,
    paddingInner = 0.1,
    paddingOuter = 0.1,
    scaleX = d3.scaleBand()
      .paddingInner(paddingInner)
      .paddingOuter(paddingOuter)
      .rangeRound([0, 0]),
    scaleY = d3.scaleLinear()
      .range([0,0]),
    labelShiftX = d3.scaleLinear()
      .range([0.5,-9])
      .domain([0,-90]),
    labelShiftY = d3.scaleLinear()
      .range([9,-0.5])
      .domain([0,-90]),
    labelShiftDY = d3.scaleLinear()
      .range([0.71,0.29])
      .domain([0,-90])

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

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

  /**
   * <p>Defines whether series will be drawn `stacked` (on top of each other) or next to each other</p>
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @param {Boolean} [stacked=true] - Should the series be plotted on top of each other
   * @method stacked
   */
  plot.stacked = function(_){
    if (arguments.length){
      stacked = _
      return plot
    } else {
      return stacked
    }
  }

  /**
   * <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.Barplot
   * @instance
   * @param {String[]} [value]
   * @method categories
   */
  plot.categories = function(_){
    if (arguments.length){
      categories = _
      return plot
    } else {
      return categories
    }
  }

  /**
   * <p>Defines if the category names of the X-axis should be rotated</p>
   * If value is specified, sets the degree of rotation.<br>
   * If value is not specified, return the current degree of roation
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @param {Int} [value]
   * @method xLabelRotation
   */
  plot.xLabelRotation = function(_){
    if (arguments.length){
      xLabelRotation = _
      return plot
    } else {
      return xLabelRotation
    }
  }

  /**
   * <p>Defines if the inner padding of the bars</p>
   * If value is specified, sets the padding between bars.<br>
   * If value is not specified, return the current inner padding
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @param {Number} [value=0.1] - between 0 - 1
   * @method paddingInner
   */
  plot.paddingInner = function(_){
    if (arguments.length){
      paddingInner = _
      scaleX.paddingInner(paddingInner)
      return plot
    } else {
      return paddingInner
    }
  }

  /**
   * <p>Defines if the outer padding of the bars</p>
   * If value is specified, sets the padding between bars.<br>
   * If value is not specified, return the current outer padding
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @param {Number} [value=0.1] - between 0 - 1
   * @method paddingOuter
   */
  plot.paddingOuter = function(_){
    if (arguments.length){
      paddingOuter = _
      scaleX.paddingOuter(paddingOuter)
      return plot
    } else {
      return paddingOuter
    }
  }

  /**
   * <p>Defines the custom Barplot specifc scale for the x-axis</p>
   * If value is specified, sets the x-axis scale.<br>
   * If value is not specified, return the current scale
   * Should not be used as setter in most cases
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @param {Function} [value]
   * @method scaleX
   */
  plot.scaleX = function(_){
    if (arguments.length){
      scaleX = _
      return plot
    } else {
      return scaleX
    }
  }

  /**
   * <p>Defines the custom Barplot specifc scale for the y-axis</p>
   * If value is specified, sets the y-axis scale.<br>
   * If value is not specified, return the current scale
   * Should not be used as setter in most cases
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @param {Function} [value]
   * @method scaleY
   */
  plot.scaleY = function(_){
    if (arguments.length){
      scaleY = _
      return plot
    } else {
      return scaleY
    }
  }


  /**
   * <p>Sets the domain for the x- and y-axis scales</p>
   * X-axis scale is defined via `categories`
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @private
   * @param {Function} [value]
   * @method setScaleDomains
   */
  plot.setScaleDomains = function(){
    scaleX.domain(categories)
    scaleY.domain(plot.yDomain())
  }

  /**
   * <p>Sets the range for the x- and y-axis scales</p>
   * X-axis gets range-round <br>
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @private
   * @method setScaleRanges
   */
  plot.setScaleRanges = function(dimensions){
    scaleX.rangeRound([0,dimensions.plotWidth])
    scaleY.range([dimensions.plotHeight,0])
  }

  /**
   * <p>Adds the X- and Y-axes to the plot</p>
   * X-axis receives custom tickValues<br>
   * Y-axis will get automated tickValues
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @private
   * @method addAxes
   */
  plot.addAxes = function(){
    plot.addXAxis(categories)
    plot.addYAxis()
    applyXLabelRotation()
  }

  plot.checkData = checkData
  plot.drawData = drawData


  return plot


  /**
   * <p>Barplot specific function to draw the actual plot</p>
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @private
   * @method drawData
   */
  function drawData(canvas, dimensions){

    // Have to call this again to override values set by axisGenerator
    applyXLabelRotation()

    var className = this.className()
    var categoryGroups = canvas.selectAll("g.category")
      .data(this.data().map(function(datum,i){
        return {
          x: categories[i],
          data: datum,
          total: datum.reduce(function(tot,x){
            return tot+x
          }),
          nSeries: datum.length,
          info : plot.info()[i] || []
        }
      }))

    categoryGroups.exit().remove()

    categoryGroups = categoryGroups.enter()
      .append("g")
      .merge(categoryGroups)
      .attr("class","esbmeplot category "+className)
      .attr("transform",function(d){
        return "translate("+
        scaleX(d.x)+
        ",0)"
      })

    var seriesNames = plot.seriesNames()
    var series = categoryGroups.selectAll("g.series").data(function(d){
      return d.data.map(function(datum,i){
        return {
          category: d.x,
          value: datum,
          seriesName: seriesNames[i],
          total: d.total,
          nSeries: d.nSeries,
          previous: d.data.slice(0,i).reduce(function(tot,x){
            return tot+x
          },0),
          info: d.info[i]
        }
      })
    })

    series.exit().remove()

    series = series.enter()
      .append("g")
      .attr("transform",function(d,i){
        var zero = {
          previous: 0,
          value: 0,
          nSeries: d.nSeries
        }
        return stackBars(zero,i,dimensions)
      })
      .merge(series)

    series
      .attr("class","esbmeplot series "+className)
      .transition().duration(plot.duration())
      .attr("transform",function(d,i){
        return stackBars(d, i, dimensions)
      })

    var bars = series.selectAll("rect.bar")
      .data(function(d){return [d]})

    bars.exit().remove()

    bars = bars.enter()
      .append("rect")
      .attr("height",0)
      .attr("width",function(d){
        if (stacked){
          return scaleX.bandwidth()
        } else {
          return scaleX.bandwidth()/d.nSeries
        }
      })
      .merge(bars)
      .attr("class",function(d){
        return "esbmeplot bar " + className + " " + d.seriesName
      })

    bars.transition().duration(plot.duration())
      .attr("height",function(d){
        return dimensions.plotHeight - scaleY(d.value)
      })
      .attr("width",function(d){
        if (stacked){
          return scaleX.bandwidth()
        } else {
          return scaleX.bandwidth()/d.nSeries
        }
      })

    plot.addEvents(bars)

    return plot
  }


  /**
   * <p>Shifts rects based on their stack status</p>
   * If `stacked` = `true`, bars will be placed on top of each other.<br>
   * Otherwise, bars will be places next to each other
   * Calculates width of bars as well.
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @private
   * @method stackBars
   */
  function stackBars(d, i, dimensions){
    var transform = "translate(",
      x = 0,
      y = 0

    if (stacked){
      y = scaleY(d.previous) - (dimensions.plotHeight - scaleY(d.value))
    } else {
      y = scaleY(d.value)
      x = scaleX.bandwidth()/d.nSeries*i
    }
    return transform + x + "," + y + ")"
  }

  /**
   * <p>Ensures the data is in the right format</p>
   * Corrects small data errors, such as not nested Array
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @private
   * @method checkData
   */
  function checkData(){
    if (!(plot.data() instanceof Array)){
      throw new TypeError("Data has to be an Array")
    }
    if (stacked){
      plot.data().forEach(function(datum){
        if (!(datum instanceof Array)){
          throw new Error("For a stacked Bar plot, the data needs to be a nested Array")
        }
      })
    }
    plot.data(plot.data().map(function(datum){
      return datum instanceof Array ?
        datum :
        [datum]
    }))

    plot.info(plot.info().map(function(info){
      return info instanceof Array ?
        info :
        [info]
    }))

    if (plot.yDomain() === "auto"){
      plot.yDomain([0,getMax()])
    }
  }

  /**
   * <p>Returns the maximum value of the dataset</p>
   * Takes into account whether bars will be stacked or not
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @private
   * @method getMax
   */
  function getMax(){
    return plot.data().reduce(function(max,cur){
      var value = stacked ?
        cur.reduce(function(insideTot,insideCur){
          return insideTot + insideCur
        }) :
        cur.reduce(function(insideMax,insideCur){
          return insideMax > insideCur ? insideMax : insideCur
        })
      return max > value ? max : value
    },0)
  }

  /**
   * <p>Rotates the x-axis category labels</p>
   * Rotates the labels if required and realigns them
   * @memberof module:EsbmePlots.Barplot
   * @instance
   * @private
   * @method applyXLabelRotation
   */
  function applyXLabelRotation(){
    var label = plot.svg().selectAll("g.x-axis g.tick text")
      .attr("transform","rotate("+xLabelRotation+")")
      .attr("text-anchor","end")
      .attr("x",labelShiftX(xLabelRotation)+"px")
      .attr("y",labelShiftY(xLabelRotation)+"px")
      .attr("dy",labelShiftDY(xLabelRotation)+"em")
    if (xLabelRotation){
      label.attr("text-anchor","end")
    } else {
      label.attr("text-anchor","middle")
    }
  }

}