# Base class for all two-dimensional basic d3 charts. This class handles axes and # margins so that subclasses can focus on the construction of particular chart # types. class Epoch.Chart.Plot extends Epoch.Chart.SVG defaults = domain: null, range: null, axes: ['left', 'bottom'] ticks: top: 14 bottom: 14 left: 5 right: 5 tickFormats: top: Epoch.Formats.regular bottom: Epoch.Formats.regular left: Epoch.Formats.si right: Epoch.Formats.si defaultAxisMargins = top: 25 right: 50 bottom: 25 left: 50 optionListeners = 'option:margins.top': 'marginsChanged' 'option:margins.right': 'marginsChanged' 'option:margins.bottom': 'marginsChanged' 'option:margins.left': 'marginsChanged' 'option:axes': 'axesChanged' 'option:ticks.top': 'ticksChanged' 'option:ticks.right': 'ticksChanged' 'option:ticks.bottom': 'ticksChanged' 'option:ticks.left': 'ticksChanged' 'option:tickFormats.top': 'tickFormatsChanged' 'option:tickFormats.right': 'tickFormatsChanged' 'option:tickFormats.bottom': 'tickFormatsChanged' 'option:tickFormats.left': 'tickFormatsChanged' 'option:domain': 'domainChanged' 'option:range': 'rangeChanged' # Creates a new plot chart. # @param [Object] options Options to use when constructing the plot. # @option options [Object] margins For setting explicit values for the top, # right, bottom, and left margins in the visualization. Normally these can # be omitted and the class will set appropriately sized margins given which # axes are specified. # @option options [Array] axes A list of axes to display (top, left, bottom, right). # @option options [Object] ticks Number of ticks to place on the top, left bottom # and right axes. # @option options [Object] tickFormats What tick formatting functions to use for # the top, bottom, left, and right axes. constructor: (@options={}) -> givenMargins = Epoch.Util.copy(@options.margins) or {} super(@options = Epoch.Util.defaults(@options, defaults)) # Margins are used in a special way and only for making room for axes. # However, a user may explicitly set margins in the options, so we need # to determine if they did so, and zero out the ones they didn't if no # axis is present. @margins = {} for pos in ['top', 'right', 'bottom', 'left'] @margins[pos] = if @options.margins? and @options.margins[pos]? @options.margins[pos] else if @hasAxis(pos) defaultAxisMargins[pos] else 6 # Add a translation for the top and left margins @g = @svg.append("g") .attr("transform", "translate(#{@margins.left}, #{@margins.top})") # Register option change events @onAll optionListeners # Sets the tick formatting function to use on the given axis. # @param [String] axis Name of the axis. # @param [Function] fn Formatting function to use. setTickFormat: (axis, fn) -> @options.tickFormats[axis] = fn # @return [Boolean] true if the chart has an axis with a given name, false otherwise. # @param [String] axis Name of axis to check. hasAxis: (axis) -> @options.axes.indexOf(axis) > -1 # @return [Number] Width of the visualization portion of the chart (width - margins). innerWidth: -> @width - (@margins.left + @margins.right) # @return [Number] Height of the visualization portion of the chart (height - margins). innerHeight: -> @height - (@margins.top + @margins.bottom) # @return [Function] The x scale for the visualization. x: -> domain = @options.domain ? @extent((d) -> d.x) d3.scale.linear() .domain(domain) .range([0, @innerWidth()]) # @return [Function] The y scale for the visualization. y: (givenDomain) -> d3.scale.linear() .domain(@_getScaleDomain(givenDomain)) .range([@innerHeight(), 0]) # @return [Function] d3 axis to use for the bottom of the visualization. bottomAxis: -> d3.svg.axis().scale(@x()).orient('bottom') .ticks(@options.ticks.bottom) .tickFormat(@options.tickFormats.bottom) # @return [Function] d3 axis to use for the top of the visualization. topAxis: -> d3.svg.axis().scale(@x()).orient('top') .ticks(@options.ticks.top) .tickFormat(@options.tickFormats.top) # @return [Function] d3 axis to use on the left of the visualization. leftAxis: -> range = if @options.range then @options.range.left else null d3.svg.axis().scale(@y(range)).orient('left') .ticks(@options.ticks.left) .tickFormat(@options.tickFormats.left) # @return [Function] d3 axis to use on the right of the visualization. rightAxis: -> range = if @options.range then @options.range.right else null d3.svg.axis().scale(@y(range)).orient('right') .ticks(@options.ticks.right) .tickFormat(@options.tickFormats.right) # Renders the axes for the visualization (subclasses must implement specific # drawing routines). draw: -> if @_axesDrawn @_redrawAxes() else @_drawAxes() super() # Redraws the axes for the visualization. _redrawAxes: -> if @hasAxis('bottom') @g.selectAll('.x.axis.bottom').transition() .duration(500) .ease('linear') .call(@bottomAxis()) if @hasAxis('top') @g.selectAll('.x.axis.top').transition() .duration(500) .ease('linear') .call(@topAxis()) if @hasAxis('left') @g.selectAll('.y.axis.left').transition() .duration(500) .ease('linear') .call(@leftAxis()) if @hasAxis('right') @g.selectAll('.y.axis.right').transition() .duration(500) .ease('linear') .call(@rightAxis()) # Draws the initial axes for the visualization. _drawAxes: -> if @hasAxis('bottom') @g.append("g") .attr("class", "x axis bottom") .attr("transform", "translate(0, #{@innerHeight()})") .call(@bottomAxis()) if @hasAxis('top') @g.append("g") .attr('class', 'x axis top') .call(@topAxis()) if @hasAxis('left') @g.append("g") .attr("class", "y axis left") .call(@leftAxis()) if @hasAxis('right') @g.append('g') .attr('class', 'y axis right') .attr('transform', "translate(#{@innerWidth()}, 0)") .call(@rightAxis()) @_axesDrawn = true dimensionsChanged: -> super() @g.selectAll('.axis').remove() @_axesDrawn = false @draw() # Updates margins in response to a option:margin.* event. marginsChanged: -> return unless @options.margins? for own pos, size of @options.margins unless size? @margins[pos] = 6 else @margins[pos] = size @g.transition() .duration(750) .attr("transform", "translate(#{@margins.left}, #{@margins.top})") @draw() # Updates axes in response to a option:axes event. axesChanged: -> # Remove default axis margins for pos in ['top', 'right', 'bottom', 'left'] continue if @options.margins? and @options.margins[pos]? if @hasAxis(pos) @margins[pos] = defaultAxisMargins[pos] else @margins[pos] = 6 # Update the margin offset @g.transition() .duration(750) .attr("transform", "translate(#{@margins.left}, #{@margins.top})") # Remove the axes and redraw @g.selectAll('.axis').remove() @_axesDrawn = false @draw() # Updates ticks in response to a option:ticks.* event. ticksChanged: -> @draw() # Updates tick formats in response to a option:tickFormats.* event. tickFormatsChanged: -> @draw() # Updates chart in response to a option:domain event. domainChanged: -> @draw() # Updates chart in response to a option:range event. rangeChanged: -> @draw() # "They will see us waving from such great heights, come down now..." - The Postal Service