# Static bar chart implementation (using d3). class Epoch.Chart.Bar extends Epoch.Chart.Plot defaults = type: 'bar' style: 'grouped' orientation: 'vertical' padding: bar: 0.08 group: 0.1 outerPadding: bar: 0.08 group: 0.1 horizontal_specific = tickFormats: top: Epoch.Formats.si bottom: Epoch.Formats.si left: Epoch.Formats.regular right: Epoch.Formats.regular horizontal_defaults = Epoch.Util.defaults(horizontal_specific, defaults) optionListeners = 'option:orientation': 'orientationChanged' 'option:padding': 'paddingChanged' 'option:outerPadding': 'paddingChanged' 'option:padding:bar': 'paddingChanged' 'option:padding:group': 'paddingChanged' 'option:outerPadding:bar': 'paddingChanged' 'option:outerPadding:group': 'paddingChanged' constructor: (@options={}) -> if @_isHorizontal() @options = Epoch.Util.defaults(@options, horizontal_defaults) else @options = Epoch.Util.defaults(@options, defaults) super(@options) @onAll optionListeners @draw() # @return [Boolean] True if the chart is vertical, false otherwise _isVertical: -> @options.orientation == 'vertical' # @return [Boolean] True if the chart is horizontal, false otherwise _isHorizontal: -> @options.orientation == 'horizontal' # @return [Function] The scale used to generate the chart's x scale. x: -> if @_isVertical() d3.scale.ordinal() .domain(Epoch.Util.domain(@getVisibleLayers())) .rangeRoundBands([0, @innerWidth()], @options.padding.group, @options.outerPadding.group) else extent = @extent((d) -> d.y) extent[0] = Math.min(0, extent[0]) d3.scale.linear() .domain(extent) .range([0, @width - @margins.left - @margins.right]) # @return [Function] The x scale used to render the horizontal bar chart. x1: (x0) -> d3.scale.ordinal() .domain((layer.category for layer in @getVisibleLayers())) .rangeRoundBands([0, x0.rangeBand()], @options.padding.bar, @options.outerPadding.bar) # @return [Function] The y scale used to render the bar chart. y: -> if @_isVertical() extent = @extent((d) -> d.y) extent[0] = Math.min(0, extent[0]) d3.scale.linear() .domain(extent) .range([@height - @margins.top - @margins.bottom, 0]) else d3.scale.ordinal() .domain(Epoch.Util.domain(@getVisibleLayers())) .rangeRoundBands([0, @innerHeight()], @options.padding.group, @options.outerPadding.group) # @return [Function] The x scale used to render the vertical bar chart. y1: (y0) -> d3.scale.ordinal() .domain((layer.category for layer in @getVisibleLayers())) .rangeRoundBands([0, y0.rangeBand()], @options.padding.bar, @options.outerPadding.bar) # Remaps the bar chart data into a form that is easier to display. # @return [Array] The reorganized data. _remapData: -> map = {} for layer in @getVisibleLayers() className = 'bar ' + layer.className.replace(/\s*layer\s*/, '') for entry in layer.values map[entry.x] ?= [] map[entry.x].push { label: layer.category, y: entry.y, className: className } ({group: k, values: v} for own k, v of map) # Draws the bar char. draw: -> if @_isVertical() @_drawVertical() else @_drawHorizontal() super() # Draws the bar chart with a vertical orientation _drawVertical: -> [x0, y] = [@x(), @y()] x1 = @x1(x0) height = @height - @margins.top - @margins.bottom data = @_remapData() # 1) Join layer = @g.selectAll(".layer") .data(data, (d) -> d.group) # 2) Update layer.transition().duration(750) .attr("transform", (d) -> "translate(#{x0(d.group)}, 0)") # 3) Enter / Create layer.enter().append("g") .attr('class', 'layer') .attr("transform", (d) -> "translate(#{x0(d.group)}, 0)") rects = layer.selectAll('rect') .data((group) -> group.values) rects.attr('class', (d) -> d.className) rects.transition().duration(600) .attr('x', (d) -> x1(d.label)) .attr('y', (d) -> y(d.y)) .attr('width', x1.rangeBand()) .attr('height', (d) -> height - y(d.y)) rects.enter().append('rect') .attr('class', (d) -> d.className) .attr('x', (d) -> x1(d.label)) .attr('y', (d) -> y(d.y)) .attr('width', x1.rangeBand()) .attr('height', (d) -> height - y(d.y)) rects.exit().transition() .duration(150) .style('opacity', '0') .remove() # 4) Update new and existing # 5) Exit / Remove layer.exit() .transition() .duration(750) .style('opacity', '0') .remove() # Draws the bar chart with a horizontal orientation _drawHorizontal: -> [x, y0] = [@x(), @y()] y1 = @y1(y0) width = @width - @margins.left - @margins.right data = @_remapData() # 1) Join layer = @g.selectAll(".layer") .data(data, (d) -> d.group) # 2) Update layer.transition().duration(750) .attr("transform", (d) -> "translate(0, #{y0(d.group)})") # 3) Enter / Create layer.enter().append("g") .attr('class', 'layer') .attr("transform", (d) -> "translate(0, #{y0(d.group)})") rects = layer.selectAll('rect') .data((group) -> group.values) rects.attr('class', (d) -> d.className) rects.transition().duration(600) .attr('x', (d) -> 0) .attr('y', (d) -> y1(d.label)) .attr('height', y1.rangeBand()) .attr('width', (d) -> x(d.y)) rects.enter().append('rect') .attr('class', (d) -> d.className) .attr('x', (d) -> 0) .attr('y', (d) -> y1(d.label)) .attr('height', y1.rangeBand()) .attr('width', (d) -> x(d.y)) rects.exit().transition() .duration(150) .style('opacity', '0') .remove() # 4) Update new and existing # 5) Exit / Remove layer.exit() .transition() .duration(750) .style('opacity', '0') .remove() # Generates specific tick marks to emulate d3's linear scale axis ticks # for ordinal scales. Note: this should only be called if the user has # defined a set number of ticks for a given axis. # @param [Number] numTicks Number of ticks to generate # @param [String] dataKey Property name of a datum to use for the tick value # @return [Array] The ticks for the given axis _getTickValues: (numTicks, dataKey='x') -> return [] unless @data[0]? total = @data[0].values.length step = Math.ceil(total / numTicks)|0 tickValues = (@data[0].values[i].x for i in [0...total] by step) # @return [Function] d3 axis to use for the bottom of the visualization. bottomAxis: -> axis = d3.svg.axis().scale(@x()).orient('bottom') .ticks(@options.ticks.bottom) .tickFormat(@options.tickFormats.bottom) if @_isVertical() and @options.ticks.bottom? axis.tickValues @_getTickValues(@options.ticks.bottom) axis # @return [Function] d3 axis to use for the top of the visualization. topAxis: -> axis = d3.svg.axis().scale(@x()).orient('top') .ticks(@options.ticks.top) .tickFormat(@options.tickFormats.top) if @_isVertical() and @options.ticks.top? axis.tickValues @_getTickValues(@options.ticks.top) axis # @return [Function] d3 axis to use on the left of the visualization. leftAxis: -> axis = d3.svg.axis().scale(@y()).orient('left') .ticks(@options.ticks.left) .tickFormat(@options.tickFormats.left) if @_isHorizontal() and @options.ticks.left? axis.tickValues @_getTickValues(@options.ticks.left) axis # @return [Function] d3 axis to use on the right of the visualization. rightAxis: -> axis = d3.svg.axis().scale(@y()).orient('right') .ticks(@options.ticks.right) .tickFormat(@options.tickFormats.right) if @_isHorizontal() and @options.ticks.right? axis.tickValues @_getTickValues(@options.ticks.right) axis # Updates orientation in response option:orientation. orientationChanged: -> top = @options.tickFormats.top bottom = @options.tickFormats.bottom left = @options.tickFormats.left right = @options.tickFormats.right @options.tickFormats.left = top @options.tickFormats.right = bottom @options.tickFormats.top = left @options.tickFormats.bottom = right @draw() # Updates padding in response to option:padding:* and option:outerPadding:*. paddingChanged: -> @draw()