Checkpoint: Events mostly flow upward.
authordsc <dsc@wikimedia.org>
Mon, 30 Apr 2012 18:59:23 +0000 (11:59 -0700)
committerdsc <dsc@wikimedia.org>
Mon, 30 Apr 2012 18:59:23 +0000 (11:59 -0700)
17 files changed:
lib/base/base-mixin.co
lib/base/base-model.co
lib/base/model-cache.co
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-model.co
lib/main-edit.co
lib/template/datasource-ui.jade
lib/util/backbone.co
lib/util/event/ready-emitter.co
lib/util/event/waiting-emitter.co

index 1dc1a2b..0b081a7 100644 (file)
@@ -44,10 +44,10 @@ BaseBackboneMixin = exports.BaseBackboneMixin =
      * Subsequent listeners added on this event will be auto-triggered.
      * @returns {this}
      */
-    triggerReady: ->
-        return this if @ready
-        @ready = true
-        @trigger 'ready', this
+    triggerReady: (lock='ready', event='ready') ->
+        return this if @[lock]
+        @[lock] = true
+        @trigger event, this
         this
     
     /**
@@ -55,10 +55,10 @@ BaseBackboneMixin = exports.BaseBackboneMixin =
      * 'ready-reset' event.
      * @returns {this}
      */
-    resetReady: ->
-        return this unless @ready
-        @ready = false
-        @trigger 'ready-reset', this
+    resetReady: (lock='ready', event='ready') ->
+        return this unless @[lock]
+        @[lock] = false
+        @trigger "#event-reset", this
         this
     
     /**
@@ -71,13 +71,18 @@ BaseBackboneMixin = exports.BaseBackboneMixin =
      * @param {Object} [context]
      * @returns {this}
      */
-    on: (events, callback, context) ->
+    on: (events, callback, context=this) ->
         return this if not callback
         Backbone.Events.on ...
         if @ready and _.contains events.split(/\s+/), 'ready'
-            callback.call context or this, this
+            callback.call context, this
         this
     
+    makeHandlersForCallback: (cb) ->
+        success : ~> cb.call this, [null].concat arguments
+        error   : ~> cb.call this, it
+    
+    
     
     
     ### Synchronization
@@ -127,6 +132,9 @@ BaseBackboneMixin = exports.BaseBackboneMixin =
             self.unwait(); fn ...
     
     
+    
+    ###
+    
     getClassName: ->
         "#{@..name or @..displayName}"
     
index 499fb7d..348ea82 100644 (file)
@@ -18,7 +18,7 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
         @__superclass__ = @..__super__.constructor
         @waitingOn      = 0
         Backbone.Model ...
-        @trigger 'create', this
+        # @..trigger 'create', this
     
     
     
@@ -52,7 +52,78 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
     #     
     
     
+    ### Data & Model Loading
+    
+    /**
+     * Override to customize what data or assets the model requires,
+     * and how they should be loaded.
+     * 
+     * By default, `load()` simply calls `loadModel()` via `loader()`.
+     * 
+     * @see BaseModel#loader
+     * @see BaseModel#loadModel
+     * @returns {this}
+     */
+    load: ->
+        console.log "#this.load()"
+        @loader do
+            start         : @loadModel
+            completeEvent : 'fetch-success'
+        this
+    
     
+    /**
+     * Wraps the loading workflow boilerplate:
+     *  - Squelches multiple loads from running at once
+     *  - Squelches loads post-ready, unless forced
+     *  - Triggers a start event
+     *  - Triggers "ready" when complete
+     *  - Wraps workflow with wait/unwait
+     *  - Cleans up "loading" state
+     * 
+     * @protected
+     * @param {Object} [opts={}] Options:
+     * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context.
+     * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load.
+     * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed.
+     * @param {Boolean} [opts.force=false] If true, move forward with the load even if we're ready.
+     * @returns {this} 
+     */
+    loader: (opts={}) ->
+        opts = { -force, startEvent:'load', completeEvent:'load-success', ...opts }
+        @resetReady() if opts.force
+        return this if not opts.start or @loading or @ready
+        
+        @wait()
+        @loading = true
+        @trigger opts.startEvent, this
+        
+        # Register a handler for the post-load event that will run only once
+        @once opts.completeEvent, ~>
+            console.log "#{this}.onLoadComplete()"
+            @loading = false
+            @unwait() # terminates the `load` wait
+            @trigger 'load-success', this unless opts.completeEvent is 'load-success'
+            @triggerReady()
+        
+        # Finally, start the loading process
+        opts.start.call this
+        this
+    
+    /**
+     * Runs `.fetch()`, triggering a `fetch` event at start, and
+     * `fetch-success` / `fetch-error` on completion.
+     * 
+     * @protected
+     * @returns {this}
+     */
+    loadModel: ->
+        @wait()
+        @trigger 'fetch', this
+        @fetch do
+            success : ~> @unwait(); @trigger 'fetch-success', this
+            error   : ~> @unwait(); @trigger 'fetch-error',   this, ...arguments
+        this
     
     
     ### Serialization
@@ -149,11 +220,11 @@ BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{
     toString: ->
         "#{@getClassName()}[#{@length}]"
     
-    # toString: ->
-    #     modelIds = @models
-    #         .map -> "\"#{it.id ? it.cid}\""
-    #         .join ', '
-    #     "#{@getClassName()}[#{@length}](#modelIds)"
+    toStringWithIds: ->
+        modelIds = @models
+            .map -> "\"#{it.id ? it.cid}\""
+            .join ', '
+        "#{@getClassName()}[#{@length}](#modelIds)"
 # }}}
 
 
index f3ef08c..f636a7d 100644 (file)
@@ -146,7 +146,7 @@ class exports.ModelCache extends ReadyEmitter
             .parMap_ (next, id) ~>
                 return next.ok(that) if @cache.get id
                 @register @createModel id
-                    .on 'ready', next
+                    .on 'ready', -> next.ok it
                     .load()
             .unflatten()
             .seq (models) ->
index 9fc9f3d..6c8d115 100644 (file)
@@ -7,13 +7,14 @@ Seq = require 'seq'
 } = require 'kraken/dataset/dataset-view'
 { MetricEditView,
 } = require 'kraken/dataset/metric-edit-view'
-
+{ DataSource, 
+} = require 'kraken/dataset/datasource-model'
 
 /**
- * @class
+ * @class DataSet selection and customization UI (root of the `data` tab).
  */
 DataView = exports.DataView = BaseView.extend do # {{{
-    __bind__       : <[ ]>
+    __bind__       : <[ onMetricsChanged ]>
     tagName        : 'section'
     className      : 'data-ui'
     template       : require 'kraken/template/data'
@@ -21,7 +22,9 @@ DataView = exports.DataView = BaseView.extend do # {{{
     datasources : null
     
     
-    
+    /**
+     * @constructor
+     */
     constructor: function DataView
         BaseView ...
     
@@ -29,20 +32,16 @@ DataView = exports.DataView = BaseView.extend do # {{{
         @graph_id = @options.graph_id
         BaseView::initialize ...
         @metric_views = new ViewList
-        @datasources = @model.sources
+        @datasources = DataSource.getAllSources()
         @model.metrics
-            .on 'add',      @addMetric,     this
-            .on 'remove',   @removeMetric,  this
-        @on 'ready', @onReady, this
-        @load()
+            .on 'add',          @addMetric,     this
+            .on 'remove',       @removeMetric,  this
+        @model.once 'ready',    @onReady,       this
     
     onReady: ->
+        console.log "#this.onReady! #{@model.metrics}"
         dataset = @model
         @model.metrics.each @addMetric, this
-        # @metric_edit_view = @addSubview new MetricEditView  {@graph_id, dataset, @datasources}
-        # @metric_edit_view
-        #     .on 'update',           @onUpdateMetric,    this
-        
         @dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources}
         @addSubview @dataset_view
             .on 'add-metric',       @onMetricsChanged,  this
@@ -50,19 +49,10 @@ DataView = exports.DataView = BaseView.extend do # {{{
             .on 'edit-metric',      @editMetric,        this
         
         @render()
+        @triggerReady()
         this
     
     
-    
-    load: ->
-        @wait()
-        # $.getJSON '/datasources/all', (@data) ~>
-        #     _.each @data, @canonicalizeDataSource, this
-        #     @model.sources.reset _.map @data, -> it
-        @unwait()
-        # @render()
-        @triggerReady()
-    
     /**
      * Transform the `columns` field to ensure an Array of {label, type} objects.
      */
@@ -89,16 +79,11 @@ DataView = exports.DataView = BaseView.extend do # {{{
         attrs = _.clone @model.attributes
         { @graph_id, @datasources } import attrs
     
-    # Don't rebuild HTML, simply notify subviews
-    # render: ->
-    #     @renderSubviews()
-    #     @trigger 'render', this
-    #     this
-    
     addMetric: (metric) ->
         console.log "#this.addMetric!", metric
         return metric if @metric_views.findByModel metric
-        view = new MetricEditView {model:metric, @graph_id, dataset, @datasources}
+        view = new MetricEditView {model:metric, @graph_id, dataset:@model, @datasources}
+        view.on 'update', @onUpdateMetric, this
         @metric_views.push @addSubview view
         metric
     
@@ -114,12 +99,11 @@ DataView = exports.DataView = BaseView.extend do # {{{
         @metric_views.invoke 'hide'
         @metric_edit_view = @metric_views.findByModel metric
         @metric_edit_view?.show()
-        @onMetricsChanged()
+        _.delay @onMetricsChanged, 10
     
     onMetricsChanged: ->
         oldMinHeight = parseInt @$el.css 'min-height'
         newMinHeight = Math.max do
-            oldMinHeight
             @dataset_view.$el.height()
             @metric_edit_view?.$el.height()
         # console.log 'onMetricsChanged!', oldMinHeight, '-->', newMinHeight
index e0722b4..bec6098 100644 (file)
@@ -17,7 +17,6 @@ ColorBrewer = require 'colorbrewer'
  */
 DataSet = exports.DataSet = BaseModel.extend do # {{{
     urlRoot : '/datasets'
-    ready : false
     
     /**
      * @type DataSourceList
@@ -40,7 +39,6 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
     
     initialize : ->
         BaseModel::initialize ...
-        @sources = new DataSourceList
         @metrics = new MetricList @attributes.metrics
         @on 'change:metrics', @onMetricChange, this
     
@@ -48,16 +46,23 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
     
     load: (opts={}) ->
         @resetReady() if opts.force
-        return this if @ready
+        return this if @loading or @ready
+        
+        unless @metrics.length
+            return @triggerReady()
+        
+        console.log "#this.load()..."
         @wait()
+        @loading = true
         @trigger 'load', this
-        Seq _.unique @metrics.pluck 'source_id'
-            .parMap_ (next, source_id) ->
-                DataSource.lookup source_id, next
-            .seqEach_ (next, source) ~>
-                @sources.add source
-                next.ok source
+        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!"
+                @loading = false
                 @unwait() # terminates the `load` wait
                 @triggerReady()
         this
@@ -106,7 +111,9 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
         m
     
     onMetricChange: ->
-        @metrics.reset @get 'metrics'
+        @resetReady()
+        # @metrics.reset @get 'metrics'
+        @load()
     
     # TODO: toJSON() must ensure columns in MetricList are ordered by index
     #   ...in theory, MetricList.comparator now does this
index b201229..ecdf08b 100644 (file)
@@ -27,7 +27,8 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
         {@graph_id, @datasources, @dataset} = @options
         BaseView::initialize ...
         @views_by_cid = {}
-        @addAllMetrics()
+        @model
+            .on 'ready',  @addAllMetrics,     this
         @model.metrics
             .on 'add',    @addMetric,         this
             .on 'remove', @removeMetric,      this
@@ -63,6 +64,7 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
         metric.view
     
     addAllMetrics: ->
+        console.log "#this.addAllMetrics! --> #{@model.metrics}"
         @removeAllSubviews()
         @model.metrics.each @addMetric, this
         this
index 7b824eb..75bf5e0 100644 (file)
@@ -2,7 +2,7 @@
 } = require 'kraken/util'
 { TimeSeriesData, CSVData,
 } = require 'kraken/timeseries'
-{ BaseModel, BaseList, BaseView,
+{ BaseModel, BaseList, ModelCache,
 } = require 'kraken/base'
 { Metric, MetricList,
 } = require 'kraken/dataset/metric-model'
@@ -12,7 +12,7 @@
  * @class
  */
 DataSource = exports.DataSource = BaseModel.extend do # {{{
-    __bind__ : <[ onLoadSuccess onLoadError ]>
+    __bind__ : <[ onLoadDataSuccess onLoadDataError ]>
     urlRoot  : '/datasources'
     ready    : false
     
@@ -61,7 +61,6 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{
         @constructor.register this
         @metrics = new MetricList @attributes.metrics
         @on 'change:metrics', @onMetricChange, this
-        # @load()
     
     
     canonicalize: (ds) ->
@@ -83,44 +82,59 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{
                 {idx, label, type:cols.types[idx] or 'int'}
         ds
     
-    load: ->
-        @trigger 'load', this
-        url = @get 'url'
+    
+    
+    loadAll: ->
+        @loader start: ->
+            Seq()
+                .seq_ (next) ~>
+                    @once 'fetch-success', next.ok
+                    @loadModel()
+                .seq_ (next) ~>
+                    @once 'load-data-success', next.ok
+                    @loadData()
+                .seq ~>
+                    @trigger 'load-success', this
+        this
+    
+    loadData: ->
+        @wait()
+        @trigger 'load-data', this
+        return @onLoadDataSuccess @data if @data
         switch @get 'format'
-        case 'json'
-            @loadJSON url
-        case 'csv'
-            @loadCSV url
+        case 'json' then @loadJSON()
+        case 'csv'  then @loadCSV()
         default
             console.error "#this.load() Unknown Data Format!"
-            @onLoadError null, 'Unknown Data Format!', new Error 'Unknown Data Format!'
+            @onLoadDataError null, 'Unknown Data Format!', new Error 'Unknown Data Format!'
         this
     
-    loadJSON: (url) ->
+    loadJSON: ->
         $.ajax do
-            url      : url
+            url      : @get 'url'
             dataType : 'json'
-            success  : (data) ~> @onLoadSuccess new TimeSeriesData data
-            error    : @onLoadError
+            success  : (data) ~> @onLoadDataSuccess new TimeSeriesData data
+            error    : @onLoadDataError
         this
     
-    loadCSV: (url) ->
+    loadCSV: ->
         $.ajax do
-            url      : url
+            url      : @get 'url'
             dataType : 'text'
-            success  : (data) ~> @onLoadSuccess new CSVData data
-            error    : @onLoadError
+            success  : (data) ~> @onLoadDataSuccess new CSVData data
+            error    : @onLoadDataError
         this
     
-    onLoadSuccess: (@data) ->
-        console.log "#this.onLoadSuccess #{@data}"
-        @trigger 'load-success', this
-        @triggerReady()
+    onLoadDataSuccess: (@data) ->
+        console.log "#this.onLoadDataSuccess #{@data}"
+        @unwait()
+        @trigger 'load-data-success', this
     
-    onLoadError: (jqXHR, txtStatus, err) ->
-        @_errorLoading = true
+    onLoadDataError: (jqXHR, txtStatus, err) ->
         console.error "#this Error loading data! -- #msg: #{err or ''}"
-        @trigger 'load-error', this, txtStatus, err
+        @unwait()
+        @_errorLoading = true
+        @trigger 'load-data-error', this, txtStatus, err
     
     
     getDateColumn: ->
@@ -142,7 +156,7 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{
     onMetricChange: ->
         @metrics.reset @get 'metrics'
     
-    
+
 # }}}
 
 
@@ -159,49 +173,17 @@ DataSourceList = exports.DataSourceList = BaseList.extend do # {{{
 
 
 
+### DataSource Cache
 
-/* * * *  DataSource Cache  * * * */
-
-DataSource import do
-    CACHE : new DataSourceList
-    ready : false
-    
-    register: (model) ->
-        return model unless model
-        # console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model
-        if @CACHE.contains model
-            @CACHE.remove model, {+silent}
-        @CACHE.add model
-        model
-    
-    get: (id) ->
-        @CACHE.get id
-    
-    lookup: (id, cb, cxt=this) ->
-        # console.log "#{@CACHE}.lookup(#id, #{typeof cb})"
-        unless @ready
-            @on 'cache-ready', ~>
-                @off 'cache-ready', arguments.callee
-                @lookup id, cb, cxt
-            return
-        
-        if @CACHE.get id
-            cb.call cxt, null, that
-        else
-            Cls = this
-            @register new Cls {id}
-                .on 'ready', -> cb.call cxt, null, it
-                .load()
-    
-
-_.bindAll DataSource, 'register', 'get', 'lookup'
-
+ALL_SOURCES = new DataSourceList
+sourceCache = new ModelCache DataSource, {-ready, cache:ALL_SOURCES}
 
 # Fetch all DataSources
 $.getJSON '/datasources/all', (data) ->
-    DataSource.CACHE.reset _.map data, -> it
-    DataSource.ready = true
-    DataSource.trigger 'cache-ready', DataSource
+    ALL_SOURCES.reset _.map data, op.I
+    sourceCache.triggerReady()
 
+DataSource.getAllSources = ->
+    ALL_SOURCES
 
 
index 5765b41..29df057 100644 (file)
@@ -6,6 +6,7 @@
 
 /**
  * @class
+ * Model is a Metric.
  */
 DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{
     __bind__       : <[  ]>
@@ -52,9 +53,17 @@ DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{
         @$el.toggleClass 'in'
     
     onSelectMetric: (evt) ->
-        tr = evt.currentTarget
-        idx = @$ '.source-metrics .datasource-source-metric' .toArray().indexOf tr
-        return unless idx is not -1
+        # tr = evt.currentTarget
+        # idx = @$ '.source-metrics .datasource-source-metric' .toArray().indexOf tr
+        # return unless idx is not -1
+        el = $ evt.currentTarget
+        {source_id, source_col} = el.data()
+        source_col = parseInt source_col
+        return if not source_id or isNaN source_col
         
+        @$ '.source-metrics .datasource-source-metric' .removeClass 'active'
+        el.addClass 'active'
+        @model.set {source_col, source_id}
+        # @trigger 'edit-metric', @model
     
 # }}}
index 47904f4..4cbdcf3 100644 (file)
 
 /**
  * @class
+ * Model is a Metric.
  */
 MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
     tagName        : 'section'
     className      : 'metric-edit-ui'
     template       : require 'kraken/template/metric-edit'
     
-    callOnReturnKeypress : 'onChanged'
+    callOnReturnKeypress : 'onChange'
     events:
         'keydown .metric-label' : 'onReturnKeypress'
     
@@ -37,18 +38,18 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
         @datasource_ui_view = new DataSourceUIView {@model, @graph_id, @dataset, @datasources}
         @addSubview @datasource_ui_view
             .on 'update', ~> @trigger 'update', this
-        @$ '.metric-datasource' .append @datasource_ui_view.render().el
+        # @$ '.metric-datasource' .append @datasource_ui_view.render().el
     
     
     toTemplateLocals: ->
         locals = BaseView::toTemplateLocals ...
         locals import { @graph_id, @dataset, @datasources }
     
-    build: ->
-        BaseView::build ...
-        if @datasource_ui_view
-            @$ '.metric-datasource' .append @datasource_ui_view.render().el
-        this
+    # build: ->
+    #     BaseView::build ...
+    #     if @datasource_ui_view
+    #         @$ '.metric-datasource' .append @datasource_ui_view.render().el
+    #     this
     
     update: ->
         color = @model.get 'color'
@@ -58,16 +59,13 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
         
         this
     
-    onChanged: ->
+    onChange: ->
         attrs = @$ 'form.metric-edit-form' .formData()
         @model.set attrs, {+silent}
         @trigger 'update', this
     
-    editMetric: (metric) ->
-        console.log "#this.editMetric!", metric
-        @datasource_ui_view.setModel @setModel metric
-        @render()
-        @show()
+    onMetricChange: (metric) ->
+        console.log "#this.onMetricChange!", metric
         this
     
 # }}}
index b1f7cd2..0810370 100644 (file)
@@ -9,7 +9,6 @@ DataSource = DataSourceList = null
  */
 Metric = exports.Metric = BaseModel.extend do # {{{
     urlRoot  : '/metrics'
-    ready : false
     
     /**
      * Data source of the Metric.
@@ -44,9 +43,9 @@ Metric = exports.Metric = BaseModel.extend do # {{{
     
     initialize : ->
         BaseModel::initialize ...
-        @on 'change:source_id',  @lookupSource,         this
-        @on 'change:source_col', @onUpdateSourceCol,    this
-        @lookupSource()
+        @on 'change:source_id',  @load,     this
+        @on 'change:source_col', @updateId, this
+        @load()
     
     
     getDateColumn: ->
@@ -55,29 +54,32 @@ Metric = exports.Metric = BaseModel.extend do # {{{
     getData: ->
         @source.getColumn @get 'source_col'
     
-    lookupSource: ->
-        if source_id = @get 'source_id'
-            @wait()
-            @updateId()
-            DataSource.lookup source_id, @onSourceReady, this
-        this
-    
-    onSourceReady: (err, source) ->
-        # console.log "#this.onSourceReady", arguments
-        @unwait()
-        if err
-            console.error "#this Error loading DataSource! #err"
-        else
-            @source = source
-            @updateId()
-            @triggerReady()
-        this
-    
     
-    onUpdateSourceCol: ->
+    load: (opts={}) ->
+        source_id = @get 'source_id'
+        @resetReady() if opts.force or @source?.id is not source_id
+        return this if not source_id or @loading or @ready
+        
+        console.log "#this.load()..."
         @updateId()
+        @loading = true
+        @wait()
+        @trigger 'load', this
+        
+        DataSource.lookup source_id, (err, source) ~>
+            # console.log "#this.onSourceReady", arguments
+            @loading = false
+            @unwait() # terminates the `load` wait
+            if err
+                console.error "#{this} Error loading DataSource! #err"
+            else
+                console.log "#{this}.load() complete!"
+                @source = source
+                @updateId()
+                @triggerReady()
         this
     
+    
     updateId: ->
         if (source_id = @get('source_id')) and (source_col = @get('source_col'))
             @id = "#source_id[#source_col]"
index e7d403f..2dd3465 100644 (file)
@@ -94,31 +94,28 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
             .on 'change:dataset',   @onModelChange, this
             .on 'change:options',   @onModelChange, this
             .on 'error',            @onModelError,  this
-            .on 'ready',            @onReady,       this
+            # .on 'ready',            @onReady,       this
         
         ### Chart Options Tab, Scaffold
         @scaffold = @addSubview new ChartOptionScaffold
-        # @$ '.graph-options-pane' .append @scaffold.el
         @scaffold.collection.reset that if o.graph_spec
-        
         @scaffold.on 'change', @onScaffoldChange
         @chartOptions @model.getOptions(), {+silent}
         
         
-        # Rerender once the tab is visible
-        # Can't use @events because we need to bind before registering
-        @$el.on 'click', '.graph-data-tab',     @onFirstClickRenderDataTab
-        @$el.on 'click', '.graph-options-tab',  @onFirstClickRenderOptionsTab
-        
         ### Graph Data UI
         @data_view = @addSubview new DataView { model:@model.get('data'), graph_id:@id }
-        # @$ '.graph-data-pane' .append @data_view.render().el
         @data_view
-            .on 'change',        @onDataChange, this
             .on 'start-waiting', @wait,         this
             .on 'stop-waiting',  @unwait,       this
+            .on 'change',        @onDataChange, this
+        
+        
+        # Rerender once the tab is visible
+        # Can't use @events because we need to bind before registering
+        @$el.on 'click', '.graph-data-tab',     @onFirstClickRenderDataTab
+        @$el.on 'click', '.graph-options-tab',  @onFirstClickRenderOptionsTab
         
-        @checkWaiting()
         
         ### Chart Viewport
         @resizeViewport()
@@ -126,6 +123,28 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         # Resize chart on window resize
         # Note: can't debounce the method itself, as the debounce wrapper returns undefined
         $ root .on 'resize', _.debounce @resizeViewport, DEBOUNCE_RENDER
+        
+        # Kick off model load chain
+        @checkWaiting()
+        Seq()
+            .seq_ (next) ~>
+                @model.on 'ready', next.ok .load()
+            .seq_ (next) ~>
+                @model.on 'data-ready', next.ok .loadData()
+            .seq ~> @onReady()
+    
+    onReady: ->
+        return if @ready
+        console.log "(#this via GraphEditView).ready!"
+        @unwait() # clears `wait()` from `initialize`
+        # @ready = @scaffold.ready = true
+        @triggerReady()
+        @scaffold.triggerReady()
+        @chartOptions @model.getOptions(), {+silent}
+        @render()
+        
+        # fix up the spinner element once the DOM is settled
+        _.delay @checkWaiting, 50
     
     
     
@@ -280,20 +299,21 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         BaseView::attachSubviews ...
         @checkWaiting()
     
-    # render: ->
-    #     return this unless @ready and not @_rendering
-    #     @_rendering = true
-    #     @wait()
-    #     @checkWaiting()
-    #     @renderDetails()
-    #     @attachSubviews()
-    #     # _.invoke @subviews, 'render'
-    #     @renderChart()
-    #     @updateURL()
-    #     @trigger 'render', this
-    #     @unwait()
-    #     @_rendering = false
-    #     this
+    render: ->
+        return this unless @ready and not @_rendering
+        @_rendering = true
+        @wait()
+        @checkWaiting()
+        # @renderDetails()
+        # @attachSubviews()
+        # _.invoke @subviews, 'render'
+        BaseView::render ...
+        @renderChart()
+        # @updateURL()
+        @trigger 'render', this
+        @unwait()
+        @_rendering = false
+        this
     
     
     /**
@@ -392,16 +412,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
     ### }}}
     ### Event Handlers {{{
     
-    onReady: ->
-        return if @ready
-        console.log "(#this via GraphEditView).ready!"
-        @ready = @scaffold.ready = true
-        @unwait() # clears `wait()` from `initialize`
-        @onSync()
-        
-        # fix up the spinner element once the DOM is settled
-        _.delay @checkWaiting, 50
-    
     onSync: ->
         return unless @ready
         console.info "#this.sync() --> success!"
index d778892..8790325 100644 (file)
@@ -29,6 +29,12 @@ Graph = exports.Graph = BaseModel.extend do # {{{
     ready : false
     
     /**
+     * Whether this Graph has loaded the actual data needed to draw the chart.
+     * @type Boolean
+     */
+    dataReady : false
+    
+    /**
      * The chart type backing this graph.
      * @type ChartType
      */
@@ -81,48 +87,48 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         BaseModel.call this, attributes, opts
     
     
-    initialize: (attributes, opts) ->
+    initialize: (attributes) ->
         BaseModel::initialize ...
-        opts = {+autoload} import (opts or {})
         
         @constructor.register this
         @parents = new GraphList
         
         # TODO: Load on-demand
-        @chartType = ChartType.lookup @get('chartType')
+        @chartType = ChartType.lookup @get 'chartType'
         
         # Insert submodels in place of JSON
-        # data = @get('data') or @get('dataset')
-        @set 'data', @dataset = new DataSet(@get('data')), {+silent}
+        @dataset = new DataSet {id:@id, ...@get 'data'}
+        @set 'data', @dataset, {+silent}
         
         @trigger 'init', this
-        @load() if opts.autoload
     
     
     load: (opts={}) ->
         return this if (@loading or @ready) and not opts.force
+        console.log "#this.load()..."
         @loading = true
         @wait()
         @trigger 'load', this
         Seq()
+            # Fetch model if 
             .seq_ (next) ~>
-                if @isNew()
-                    next.ok()
-                else
-                    console.log "#{this}.fetch()..."
-                    @wait()
-                    @fetch do
-                        error : @unwaitAnd (err) ~>
-                            console.error "#{this}.fetch() --> error! #arguments"
-                            next.ok()
-                        success : @unwaitAnd (model, res) ~>
-                            # console.log "#{this}.fetch() --> success!", res
-                            @dataset.set @get('data')
-                            @trigger 'change:data', this, @dataset, 'data'
-                            @trigger 'change',      this, @dataset, 'data'
-                            next.ok res
+                return next.ok() if @isNew()
+                console.log "#{this}.fetch()..."
+                @wait()
+                @fetch do
+                    error : @unwaitAnd (err) ~>
+                        console.error "#{this}.fetch() --> error! #arguments"
+                        next.ok()
+                    success : @unwaitAnd (model, res) ~>
+                        # console.log "#{this}.fetch() --> success!", res
+                        @dataset.set @get 'data'
+                        @trigger 'change:data', this, @dataset, 'data'
+                        @trigger 'change',      this, @dataset, 'data'
+                        next.ok res
+            
+            # Load Parents...
             .seq_ (next) ~>
-                next.ok @get('parents')
+                next.ok @get 'parents'
             .flatten()
             .seqMap_ (next, parent_id) ~>
                 @wait()
@@ -132,13 +138,45 @@ Graph = exports.Graph = BaseModel.extend do # {{{
                 @optionCascade.addLookup parent.get('options')
                 @unwait()
                 next.ok()
+            
+            # Load DataSet...
+            .seq_ (next) ~>
+                @dataset.once 'ready', next.ok .load()
+            
+            # Done!
             .seq ~>
+                console.log "#{this}.load() complete!"
                 @loading = false
                 @unwait() # terminates the `load` wait
                 @triggerReady()
         this
     
     
+    loadData: (opts={}) ->
+        @resetReady 'dataReady', 'data-ready' if opts.force
+        return this if @loading or @dataReady
+        
+        unless @dataset.metrics.length
+            return @triggerReady 'dataReady', 'data-ready'
+        
+        console.log "#this.loadData()..."
+        @wait()
+        @loading = true
+        @trigger 'load-data', this
+        Seq @dataset.metrics.models
+            .parEach_ (next, metric) ->
+                metric.once 'ready', next.ok .load()
+            .parEach_ (next, metric) ->
+                metric.source
+                    .on 'load-data-success', next.ok .loadData()
+            .seq ~>
+                console.log "#{this}.loadData() complete!"
+                @loading = false
+                @unwait() # terminates the `load` wait
+                @triggerReady 'dataReady', 'data-ready'
+        this
+    
+    
     
     ### Accessors
     
@@ -308,7 +346,10 @@ Graph = exports.Graph = BaseModel.extend do # {{{
     toPermalink: ->
         "#{root.location.protocol}//#{window.location.host}#{@toLink()}"
     
-    
+
+### Graph Cache for parent-lookup
+new ModelCache Graph
+
 # }}}
 
 
@@ -318,15 +359,8 @@ GraphList = exports.GraphList = BaseList.extend do # {{{
     
     constructor : function GraphList then BaseList ...
     initialize  : -> BaseList::initialize ...
-    
-    toString: ->
-        modelIds = @models
-            .map -> "\"#{it.id ? it.cid}\""
-            .join ', '
-        "#{@getClassName()}(#modelIds)"
+    toString: -> @toStringWithIds()
 # }}}
 
 
-### Graph Cache for parent-lookup
-new ModelCache Graph
 
index d020caa..64b54cf 100644 (file)
@@ -3,15 +3,9 @@ Backbone = require 'backbone'
 
 { _, op,
 } = require 'kraken/util'
-{ TimeSeriesData, CSVData,
-} = require 'kraken/timeseries'
 { BaseView, BaseModel, BaseList,
 } = require 'kraken/base'
-{ Field, FieldList, FieldView, Scaffold,
-} = require 'kraken/scaffold'
 { ChartType, DygraphsChartType,
-  ChartOption, ChartOptionList, TagSet,
-  ChartOptionView, ChartOptionScaffold,
 } = require 'kraken/chart'
 { DataSource, DataSourceList,
 } = require 'kraken/dataset'
@@ -75,6 +69,5 @@ Seq([   <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>
             next.ok()
         error : (err) -> console.error err
 .seq ->
-    console.log 'All data loaded!'
     jQuery main
 
index 13c2ec5..f04e330 100644 (file)
@@ -1,6 +1,6 @@
-section.datasource-ui
+section.datasource-ui(class="datasource-ui-#{source_id}")
     
-    section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui .datasource-selector")
+    section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui.datasource-ui-#{source_id} .datasource-selector")
         i.expand-datasource-ui-button.icon-chevron-down
         i.collapse-datasource-ui-button.icon-chevron-up
         
@@ -48,7 +48,7 @@ section.datasource-ui
                                     tbody.source-metrics
                                         for m, idx in ds.metrics.slice(1)
                                             - var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
-                                            tr.datasource-source-metric(class=activeColClass)
+                                            tr.datasource-source-metric(class=activeColClass, data-source_id=ds.id, data-source_col=m.idx)
                                                 td.source-metric-idx #{m.idx}
                                                 td.source-metric-label #{m.label}
                                                 td.source-metric-type #{m.type}
index 58f2688..f87fda5 100644 (file)
@@ -13,12 +13,26 @@ Backbone.setDomLibrary that if window? and (window.jQuery or window.Zepto or win
 
 
 _bb_events =
+    
+    /**
+     * Registers an event listener on the given event(s) to be fired only once.
+     * 
+     * @param {String} events Space delimited list of event names.
+     * @param {Function} callback Event listener function.
+     * @param {Object} [context=this] Object to be supplied as the context for the listener.
+     * @returns {this}
+     */
     once: (events, callback, context) ->
         fn = ~>
             @off events, arguments.callee, this
             callback.apply (context or this), arguments
         @on events, fn, this
-        fn
+        this
+    
+    /**
+     * Compatibility with Node's `EventEmitter`.
+     */
+    emit: Backbone.Events.trigger
 
 
 
index fb24be4..d3f893e 100644 (file)
@@ -1,4 +1,5 @@
 {EventEmitter} = require 'events'
+EventEmitter::trigger = EventEmitter::emit
 
 
 /**
index fc98aa6..3cc2733 100644 (file)
@@ -1,4 +1,6 @@
 {EventEmitter} = require 'events'
+EventEmitter::trigger = EventEmitter::emit
+
 
 
 /**