Fixes metric CRUD in data-ui.
authorDavid Schoonover <dsc@wikimedia.org>
Tue, 22 May 2012 15:52:56 +0000 (08:52 -0700)
committerDavid Schoonover <dsc@wikimedia.org>
Tue, 22 May 2012 15:52:56 +0000 (08:52 -0700)
28 files changed:
lib/base/base-view.co
lib/chart/chart-option-view.co
lib/chart/chart-type.co
lib/chart/dygraphs.co
lib/chart/type/d3-bar.co [new file with mode: 0644]
lib/chart/type/d3-geo.co [new file with mode: 0644]
lib/chart/type/d3-line.co [new file with mode: 0644]
lib/chart/type/index.co [new file with mode: 0644]
lib/dataset/data-view.co
lib/dataset/dataset-model.co
lib/dataset/dataset-view.co
lib/dataset/datasource-model.co
lib/dataset/datasource-ui-view.co
lib/dataset/metric-edit-view.co
lib/dataset/metric-model.co
lib/graph/graph-edit-view.co
lib/graph/graph-list-view.co
lib/graph/graph-model.co
lib/main-display.co
lib/main-edit.co
lib/scaffold/scaffold-view.co
lib/template/dataset-metric.jade
lib/template/dataset.jade
lib/template/datasource-ui.jade
lib/template/datasource.jade
lib/util/backbone.co
lib/util/formatters.co [new file with mode: 0644]
lib/util/underscore/object.co

index a8d3ec9..16fcd57 100644 (file)
@@ -111,7 +111,7 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
     
     removeAllSubviews: ->
         @subviews.forEach @removeSubview, this
-        @subviews = new ViewList
+        # @subviews = new ViewList
         this
     
     
@@ -232,12 +232,12 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
     
     /* * * *  Events  * * * */
     
-    bubbleEvent: (evt) ->
+    bubbleEventDown: (evt) ->
         @invokeSubviews 'trigger', ...arguments
         this
     
-    redispatch: (evt) ->
-        @trigger ...arguments
+    redispatch: (evt, ...args) ->
+        @trigger evt, this, ...args
         this
     
     onlyOnReturn: (fn, ...args) ->
index 5a6f84f..1fde054 100644 (file)
@@ -43,7 +43,8 @@ ChartOptionView = exports.ChartOptionView = BaseView.extend do # {{{
         json = ChartOptionView.__super__.toTemplateLocals ...
         json.id or= _.camelize json.name
         json.value ?= ''
-        json.value = JSON.stringify v if v = json.value and (_.isArray(v) or _.isPlainObject(v))
+        v = json.value
+        json.value = JSON.stringify(v) if v and ( _.isArray(v) or _.isPlainObject(v) )
         json
     
     /**
index 68357c2..aea28cd 100644 (file)
@@ -70,7 +70,7 @@ class exports.ChartType extends ReadyEmitter
      * @type String
      * @readonly
      */
-    CHART_SPEC_URL : null
+    SPEC_URL : null
     
     /**
      * Chart-type name.
@@ -160,8 +160,9 @@ class exports.ChartType extends ReadyEmitter
         return this if @ready
         proto = @constructor::
         jQuery.ajax do
-            url     : @CHART_SPEC_URL
+            url     : @SPEC_URL
             success : (spec) ~>
+                proto.spec = spec
                 proto.options_ordered = spec
                 proto.options = _.synthesize spec, -> [it.name, it]
                 proto.ready = true
@@ -351,6 +352,7 @@ class exports.ChartType extends ReadyEmitter
         data       = @getData()
         options    = @getDefaultOptions() import @transform @model, @view
         viewport   = @getElementsForRole 'viewport'
+        return @lastChart unless data?.length and viewport?.length
         @lastChart = @renderChart data, viewport, options, @chart
     
     
index dd95a64..49c3f86 100644 (file)
@@ -5,7 +5,9 @@ _ = require 'kraken/util/underscore'
 
 class exports.DygraphsChartType extends ChartType
     __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]>
-    CHART_SPEC_URL : '/schema/dygraph.json'
+    SPEC_URL : '/schema/dygraph.json'
+    
+    # NOTE: ChartType.register() must come AFTER `typeName` declaration.
     typeName : 'dygraphs'
     ChartType.register this
     
@@ -93,6 +95,9 @@ class exports.DygraphsChartType extends ChartType
         { width, height }
     
     
+    /**
+     * Resizes the HTML viewport.
+     */
     resizeViewport: ->
         size = @determineSize()
         @getElementsForRole 'viewport' .css size
diff --git a/lib/chart/type/d3-bar.co b/lib/chart/type/d3-bar.co
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/chart/type/d3-geo.co b/lib/chart/type/d3-geo.co
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/chart/type/d3-line.co b/lib/chart/type/d3-line.co
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/chart/type/index.co b/lib/chart/type/index.co
new file mode 100644 (file)
index 0000000..e69de29
index b947e9a..7201d84 100644 (file)
@@ -40,7 +40,7 @@ DataView = exports.DataView = BaseView.extend do # {{{
         @model.once 'ready',    @onReady,       this
     
     onReady: ->
-        console.log "#this.onReady! #{@model.metrics}"
+        # console.log "#this.onReady! #{@model.metrics}"
         dataset = @model
         @model.metrics.each @addMetric, this
         @dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources}
@@ -81,7 +81,7 @@ DataView = exports.DataView = BaseView.extend do # {{{
         { @graph_id, @datasources } import attrs
     
     addMetric: (metric) ->
-        console.log "#this.addMetric!", metric
+        # console.log "#this.addMetric!", metric
         return metric if @metric_views.findByModel metric
         view = new MetricEditView {model:metric, @graph_id, dataset:@model, @datasources}
             .on 'metric-update', @onUpdateMetric, this
@@ -91,7 +91,7 @@ DataView = exports.DataView = BaseView.extend do # {{{
         metric
     
     removeMetric: (metric) ->
-        console.log "#this.removeMetric!", metric
+        # console.log "#this.removeMetric!", metric
         return unless view = @metric_views.findByModel metric
         @metric_views.remove view
         @removeSubview view
@@ -114,7 +114,7 @@ DataView = exports.DataView = BaseView.extend do # {{{
         @$el.css 'min-height', newMinHeight
     
     onUpdateMetric: ->
-        console.log "#this.onUpdateMetric!"
+        # console.log "#this.onUpdateMetric!"
         @trigger 'metric-change', @model, this
         @render()
     
index c797e44..dfe8aa9 100644 (file)
@@ -42,6 +42,8 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
         BaseModel::initialize ...
         @set 'metrics', @metrics, {+silent}
         @on 'change:metrics', @onMetricChange, this
+        # @metrics.on 'add remove reset', ~>
+        #     @trigger 'change:metrics', @metrics, this
     
     
     load: (opts={}) ->
@@ -51,17 +53,15 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
         unless @metrics.length
             return @triggerReady()
         
-        console.log "#this.load()..."
+        # console.log "#this.load()..."
         @wait()
         @loading = true
         @trigger 'load', this
         Seq @metrics.models
             .parEach_ (next, metric) ->
                 metric.once 'ready', next.ok .load()
-            # .parEach_ (next, metric) ->
-            #     metric.on 'load-data-success', next.ok .loadData()
             .seq ~>
-                console.log "#{this}.load() complete!"
+                # console.log "#{this}.load() complete!"
                 @loading = false
                 @unwait() # terminates the `load` wait
                 @triggerReady()
@@ -107,7 +107,11 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
      */
     getData: ->
         return [] unless @ready
-        _.zip ...@getColumns()
+        columns = @getColumns()
+        if columns?.length
+            _.zip ...columns
+        else
+            []
     
     /**
      * @returns {Array<Array>} List of all columns (including date column).
@@ -149,13 +153,13 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
     newMetric: ->
         index = @metrics.length
         @metrics.add m = new Metric { index, color:ColorBrewer.Spectral[11][index] }
-        # @get 'metrics' .push m.attributes
+        m.on 'ready', ~> @trigger 'metric-data-loaded', this, m
         # @trigger 'change:metrics',  this, @metrics, 'metrics'
         # @trigger 'change',          this, @metrics, 'metrics'
         m
     
     onMetricChange: ->
-        console.log "#this.onMetricChange! ready=#{@ready}"
+        # console.log "#this.onMetricChange! ready=#{@ready}"
         @resetReady()
         @load()
     
index 34d7ff7..bc8189f 100644 (file)
@@ -13,8 +13,9 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
     template  : require 'kraken/template/dataset'
     
     events:
-        'click .new-metric-button'       : 'newMetric'
-        'click .metrics .dataset-metric' : 'selectMetric'
+        'click  .new-metric-button'                     : 'onNewMetric'
+        'click  .delete-metric-button'                  : 'onDeleteMetric'
+        'click  .metrics .dataset-metric' : 'selectMetric'
     
     views_by_cid : {}
     active_view : null
@@ -32,30 +33,27 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
         @model.metrics
             .on 'add',    @addMetric,         this
             .on 'remove', @removeMetric,      this
+            .on 'change', @onMetricChange,    this
             .on 'reset',  @addAllMetrics,     this
     
     
-    newMetric: ->
-        console.log "#this.newMetric!"
-        # triggers 'add' on @model.metrics
-        @model.newMetric()
-        false
-    
     addMetric: (metric) ->
-        console.log "#this.addMetric!", metric
+        # console.log "#this.addMetric!", metric
         if @views_by_cid[metric.cid]
             @removeSubview that
             delete @views_by_cid[metric.cid]
         
         view = @addSubview new DataSetMetricView {model:metric, @graph_id}
         @views_by_cid[metric.cid] = view
-        # @$ '.metrics' .append view.render().el
         @trigger 'add-metric', metric, view, this
         @render()
         view
     
     removeMetric: (metric) ->
-        console.log "#this.removeMetric!", metric
+        if metric instanceof [jQuery.Event, Event]
+            metric = @getMetricForElement metric.target
+        # console.log "#this.removeMetric!", metric
+        return unless metric
         if view = @views_by_cid[metric.cid]
             @removeSubview view
             delete @views_by_cid[metric.cid]
@@ -63,24 +61,46 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
         view
     
     addAllMetrics: ->
-        console.log "#this.addAllMetrics! --> #{@model.metrics}"
+        # console.log "#this.addAllMetrics! --> #{@model.metrics}"
         @removeAllSubviews()
         @model.metrics.each @addMetric, this
         this
     
+    
     selectMetric: (metric) ->
         if metric instanceof [jQuery.Event, Event]
-            metric = $ metric.currentTarget .data 'model'
-        view = @active_view = @views_by_cid[metric.cid]
+            metric = @getMetricForElement metric.target
         # console.log "#this.selectMetric!", metric
+        return unless metric
+        view = @active_view = @views_by_cid[metric.cid]
         
         @$ '.metrics .dataset-metric' .removeClass 'metric-active'
         view.$el.addClass 'metric-active'
-        view.$el.find '.activity-arrow' .css 'font-size', 2+view.$el.height()
+        view.$ '.activity-arrow' .css 'font-size', 2+view.$el.height()
         
         @trigger 'select-metric', metric, view, this
         this
     
+    onMetricChange: (metric) ->
+        return unless view = @views_by_cid[metric?.cid]
+        view.$ '.activity-arrow:visible' .css 'font-size', 2+view.$el.height()
+    
+    onNewMetric: ->
+        # console.log "#this.newMetric!"
+        # triggers 'add' on @model.metrics
+        @model.newMetric()
+        false
+    
+    onDeleteMetric: (evt) ->
+        metric = @getMetricForElement evt.target
+        # console.log "#this.onDeleteMetric!", metric
+        # Triggers a 'remove' event, which in turn calls `removeMetric()`
+        @model.metrics.remove metric
+        false
+    
+    
+    getMetricForElement: (el) ->
+        $ el .parents '.dataset-metric' .eq(0).data 'model'
     
 # }}}
 
index 75bf5e0..19f6bfc 100644 (file)
@@ -129,6 +129,7 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{
         console.log "#this.onLoadDataSuccess #{@data}"
         @unwait()
         @trigger 'load-data-success', this
+        @triggerReady()
     
     onLoadDataError: (jqXHR, txtStatus, err) ->
         console.error "#this Error loading data! -- #msg: #{err or ''}"
index 0a69ba1..f40c69b 100644 (file)
@@ -33,7 +33,7 @@ DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{
     
     toTemplateLocals: ->
         locals = @model.toJSON()
-        locals import {@graph_id, @dataset, @datasources}
+        locals import {@graph_id, @dataset, @datasources, cid:@model.cid}
         
         ds = @model.source
         hasSource = @model.get('source_id')? and ds
index 4b5be60..f1b0fef 100644 (file)
@@ -64,7 +64,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
         this
     
     onAttach: ->
-        console.log "#this.onAttach!"
+        # console.log "#this.onAttach!"
         @$ '.color-swatch'
             .data 'color', @model.get 'color'
             .colorpicker()
index 6f3b311..2757b5b 100644 (file)
@@ -129,7 +129,7 @@ Metric = exports.Metric = BaseModel.extend do # {{{
      * attempt to graph unconfigured crap.
      */
     isOk: ->
-        @source # and _.every @get('timespan'), op.ok
+        @source?.ready # and _.every @get('timespan'), op.ok
     
 # }}}
 
index 1b70b4d..aa260f5 100644 (file)
@@ -21,7 +21,10 @@ root = do -> this
  * - Chart options, using ChartOptionScaffold
  */
 GraphEditView = exports.GraphEditView = GraphView.extend do # {{{
-    __bind__  : <[ wait unwait onScaffoldChange onFirstClickRenderOptionsTab onFirstClickRenderDataTab ]>
+    __bind__  : <[
+        wait unwait onChartTypeReady onScaffoldChange
+        onFirstClickRenderOptionsTab onFirstClickRenderDataTab
+    ]>
     className : 'graph-edit graph'
     template  : require 'kraken/template/graph-edit'
     
@@ -53,9 +56,9 @@ GraphEditView = exports.GraphEditView = GraphView.extend do # {{{
         
         ### Chart Options Tab, Scaffold
         @scaffold = @addSubview new ChartOptionScaffold
-        @scaffold.collection.reset that if o.graph_spec
-        @scaffold.on 'change', @onScaffoldChange
-        @chartOptions @model.getOptions(), {+silent}
+        # @scaffold.collection.reset that if o.graph_spec
+        # @scaffold.on 'change', @onScaffoldChange
+        @chartType.on 'ready', @onChartTypeReady
         
         
         ### Graph Data UI
@@ -70,10 +73,14 @@ GraphEditView = exports.GraphEditView = GraphView.extend do # {{{
         @$el.on 'click', '.graph-data-tab',     @onFirstClickRenderDataTab
         @$el.on 'click', '.graph-options-tab',  @onFirstClickRenderOptionsTab
         
-        
         # Kick off model load chain
         @loadData()
     
+    onChartTypeReady: ->
+        @scaffold.collection.reset @chartType.options_ordered
+        @scaffold.on 'change', @onScaffoldChange
+        @chartOptions @model.getOptions(), {+silent}
+    
     onReady: ->
         return if @ready
         console.log "(#this via GraphEditView).ready!"
@@ -84,6 +91,11 @@ GraphEditView = exports.GraphEditView = GraphView.extend do # {{{
         @chartOptions @model.getOptions(), {+silent}
         @render()
         
+        @model.dataset.metrics
+            .on 'add remove change', @render, this
+        @model
+            .on 'metric-data-loaded', @render, this
+        
         # fix up the spinner element once the DOM is settled
         _.delay @checkWaiting, 50
     
index 6ce2b93..952845c 100644 (file)
@@ -27,7 +27,7 @@ GraphListView = exports.GraphListView = BaseView.extend do # {{{
     initialize : ->
         @model = @collection or= new GraphList
         BaseView::initialize ...
-        console.log "#this.initialize!"
+        # console.log "#this.initialize!"
     
     toTemplateLocals: ->
         locals = BaseView::toTemplateLocals ...
index 2f96a1b..0a2e02e 100644 (file)
@@ -96,7 +96,9 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         
         # Insert submodels in place of JSON
         @dataset = new DataSet {id:@id, ...@get 'data'}
-            # .on 'change', @onDataSetChange, this
+            .on 'change',               @onDataSetChange, this
+            .on 'metric-data-loaded',   (dataset, metric) ~>
+                @trigger 'metric-data-loaded', this, metric
         @set 'data', @dataset, {+silent}
         
         @trigger 'init', this
@@ -112,7 +114,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
             # Fetch model if 
             .seq_ (next) ~>
                 return next.ok() if @isNew()
-                console.log "#{this}.fetch()..."
+                # console.log "#{this}.fetch()..."
                 @wait()
                 @fetch do
                     error : @unwaitAnd (err) ~>
@@ -144,7 +146,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
             
             # Done!
             .seq ~>
-                console.log "#{this}.load() complete!"
+                # console.log "#{this}.load() complete!"
                 @loading = false
                 @unwait() # terminates the `load` wait
                 @triggerReady()
@@ -169,7 +171,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
                 unless metric.source
                     console.warn "#{this}.loadData() -- Skipping metric #metric with invalid source!", metric
                     return next.ok()
-                metric.source.on 'load-data-success', next.ok .loadData()
+                metric.source.once 'load-data-success', next.ok .loadData()
             .seq ~>
                 console.log "#{this}.loadData() complete!"
                 @loading = false
@@ -182,7 +184,9 @@ Graph = exports.Graph = BaseModel.extend do # {{{
     
     onDataSetChange: ->
         console.log "#this.onDataSetChange!"
-        @set 'data', @dataset, {+silent}
+        # @set 'data', @dataset, {+silent}
+        # @trigger 'change:data', this, @dataset, 'data'
+        @trigger 'change',      this, @dataset, 'data'
     
     
     ### Accessors
@@ -246,21 +250,26 @@ Graph = exports.Graph = BaseModel.extend do # {{{
             @trigger "change:options.#key", this, value, key, opts unless opts.silent
         
         if changed and not opts.silent
-            @trigger "change:options", this, options, 'options', opts
+            @trigger "change:options",  this, options, 'options', opts
+            @trigger "change",          this, options, 'options', opts
         this
     
     unsetOption: (key, opts={}) ->
         unless @optionCascade.unset(key) is void or opts.silent
+            options = @get 'options'
             @trigger "change:options.#key", this, void, key, opts
-            @trigger "change:options", this, @get('options'), 'options', opts
+            @trigger "change:options",      this, options, 'options', opts
+            @trigger "change",              this, options, 'options', opts
         this
     
     inheritOption: (key, opts={}) ->
         old = @getOption(key)
         @optionCascade.inherit(key)
         unless @getOption(key) is old or opts.silent
+            options = @get 'options'
             @trigger "change:options.#key", this, void, key, opts
-            @trigger "change:options", this, @get('options'), 'options', opts
+            @trigger "change:options",      this, options, 'options', opts
+            @trigger "change:options",      this, options, 'options', opts
         this
     
     getOptions: (opts={}) ->
index 22de261..104a63b 100644 (file)
@@ -24,7 +24,7 @@ main = ->
     
     # Bind to URL changes
     History.Adapter.bind window, 'statechange', ->
-        console.log 'StateChange!\n\n', String(root.location), '\n\n'
+        # console.log 'StateChange!\n\n', String(root.location), '\n\n'
     
     
     
@@ -69,6 +69,6 @@ Seq([   <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>
             next.ok()
         error : (err) -> console.error err
 .seq ->
-    console.log 'All data loaded!'
+    # console.log 'All data loaded!'
     jQuery main
 
index 16e4151..ee463f3 100644 (file)
@@ -26,7 +26,7 @@ main = ->
     
     # Bind to URL changes
     History.Adapter.bind window, 'statechange', ->
-        console.log 'StateChange!\n\n', String(root.location), '\n\n'
+        # console.log 'StateChange!\n\n', String(root.location), '\n\n'
     
     
     
index 2a44f93..4c187f9 100644 (file)
@@ -33,7 +33,7 @@ FieldView = exports.FieldView = BaseView.extend do # {{{
         
         current = @model.getValue()
         return if _.isEqual val, current
-        console.log "#this.onChange( #current -> #val )"
+        # console.log "#this.onChange( #current -> #val )"
         @model.setValue val, {+silent}
         @trigger 'change', this
     
index 6209be4..0fd3b8f 100644 (file)
@@ -3,6 +3,6 @@ tr.dataset-metric(class=viewClasses)
     td.col-source(data-bind="source") #{source}
     td.col-time(data-bind="timespan", data-bind-escape="false") #{timespan}
     td.col-actions
-        a.delete-metric-button.close(href="#") &times;
+        a.delete-metric-button.control.close(href="#") &times;
         .activity-arrow: div.inner
     
index 289d571..94e5db0 100644 (file)
@@ -19,7 +19,6 @@ section.dataset-ui.dataset: div.inner
                     th.col-times    Timespan
                     th.col-actions  Actions
             tbody.metrics(data-subview="DataSetMetricView")
-                //- DataSetMetricViews attach here
     
 
 
index 94d6dbb..a934936 100644 (file)
@@ -1,6 +1,6 @@
-section.datasource-ui(class="datasource-ui-#{source_id}")
+section.datasource-ui(class="datasource-ui-#{cid}")
     
-    section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui.datasource-ui-#{source_id} .datasource-selector")
+    section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui.datasource-ui-#{cid} .datasource-selector")
         i.expand-datasource-ui-button.icon-chevron-down
         i.collapse-datasource-ui-button.icon-chevron-up
         
index eec762a..6320e72 100644 (file)
@@ -1,6 +1,7 @@
 - var ds = source.attributes
+- var id = ds.id || source.cid
 - var activeClass = (source_id === ds.id ? 'active' : '')
-.datasource-source.tab-pane(class="datasource-source-#{ds.id} #{activeClass}")
+.datasource-source.tab-pane(class="datasource-source-#{id} #{activeClass}")
     .datasource-source-details.well
         .source-name #{ds.name}
         .source-id #{ds.id}
index e8a0daf..e3d5d28 100644 (file)
@@ -56,8 +56,11 @@ _backbone = do
         if Cls.__superclass__
             superclass = that
         else
-            Cls = Cls.constructor unless typeof Cls is 'function'
-            superclass = Cls.__super__?.constructor
+            Cls = Cls.constructor if typeof Cls is not 'function'
+            if Cls.__super__?.constructor
+                superclass = that
+            else if Cls::constructor is not Cls
+                superclass
         
         if superclass
             [superclass].concat getSuperClasses superclass
diff --git a/lib/util/formatters.co b/lib/util/formatters.co
new file mode 100644 (file)
index 0000000..b30650c
--- /dev/null
@@ -0,0 +1,64 @@
+moment = require 'moment'
+
+{ _, op,
+} = require 'kraken/util'
+
+
+_fmt = do
+    
+    /**
+     * Formats a date for display on an axis: `MM/YYYY`
+     * @param {Date} d Date to format.
+     * @returns {String}
+     */
+    axisDateFormatter: (d) ->
+        moment(d).format 'MM/YYYY'
+    
+    /**
+     * Formats a date for display in the legend: `DD MMM YYYY`
+     * @param {Date} d Date to format.
+     * @returns {String}
+     */
+    dateFormatter: (d) ->
+        moment(d).format 'DD MMM YYYY'
+    
+    /**
+     * Formats a number for display, first dividing by the greatest suffix
+     *  of {B = Billions, M = Millions, K = Thousands} that results in a
+     *  absolute value greater than 0, and then rounding to `digits` using
+     *  `result.toFixed(digits)`.
+     * 
+     * @param {Number} n Number to format.
+     * @param {Number} [digits=2] Number of digits after the decimal to always display.
+     * @returns {String} Formatted number.
+     */
+    _numberFormatter: (n, digits=2) ->
+        for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]]
+            break if isNaN d
+            if n >= d
+                n = n / d
+                break
+        s = n.toFixed(digits)
+        parts = s.split '.'
+        whole = _.rchop parts[0], 3 .join ','
+        fraction = '.' + parts.slice(1).join '.'
+        { n, digits, whole, fraction, suffix }
+    
+    
+    numberFormatter: (n, digits=2) ->
+        { whole, fraction, suffix } = _fmt._numberFormatter n, digits
+        "#whole#fraction#suffix"
+    
+    numberFormatterHTML: (n, digits=2) ->
+        { whole, fraction, suffix } = _fmt._numberFormatter n, digits
+        # coco will trim the whitespace
+        "<span class='value'>
+        <span class='whole'>#whole</span>
+        <span class='fraction'>#fraction</span>
+        <span class='suffix'>#suffix</span>
+        </span>"
+    
+    
+
+
+exports = _fmt
index 4e49c46..6f07576 100644 (file)
@@ -63,7 +63,7 @@ _obj = do
      */
     remove: (obj, v) ->
         values = [].slice.call arguments, 1
-        if _.isArray obj
+        if _.isArray(obj) or obj instanceof Array
             for v of values
                 idx = obj.indexOf v
                 obj.splice idx, 1 if idx is not -1