Checkpoint on Data UI
authordsc <dsc@wikimedia.org>
Wed, 11 Apr 2012 09:01:47 +0000 (02:01 -0700)
committerdsc <dsc@wikimedia.org>
Wed, 11 Apr 2012 09:01:47 +0000 (02:01 -0700)
32 files changed:
data/graphs/ohai.json
docs/notes.md
lib/base.co
lib/chart/chart-option-model.co
lib/chart/chart-option-view.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/datasource-view.co
lib/dataset/metric-edit-view.co
lib/dataset/metric-model.co
lib/graph/graph-display-view.co
lib/graph/graph-edit-view.co
lib/graph/graph-model.co
lib/main-display.co
lib/main-edit.co
lib/scaffold/scaffold-model.co
lib/scaffold/scaffold-view.co
lib/template/chart-scaffold.jade
lib/template/data.jade
lib/template/dataset.jade
lib/template/datasource-ui.jade
lib/template/graph-display.jade
lib/template/graph-edit.jade
lib/template/metric-edit.jade
lib/util/backbone.co
www/css/data.styl
www/css/graph.styl
www/css/layout.styl
www/modules.yaml

index ea32d4f..311a2eb 100644 (file)
@@ -1 +1,111 @@
-{"options":{"animatedZooms":true,"avoidMinZero":false,"axis":null,"axisLabelColor":"#666666","axisLabelFontSize":14,"axisLabelFormatter":null,"axisLabelWidth":50,"axisLineColor":"#AAAAAA","axisLineWidth":0.3,"axisTickSize":3,"colorSaturation":1,"colorValue":0.5,"colors":["#FF0097","#EF8158","#83BB32","#182B53","#4596FF","#553DC9","#AD3238","#00FFBC","#F1D950"],"connectSeparatedPoints":false,"customBars":false,"dateWindow":null,"delimiter":",","digitsAfterDecimal":2,"displayAnnotations":false,"drawPoints":true,"drawXAxis":true,"drawXGrid":true,"drawYAxis":true,"drawYGrid":true,"errorBars":false,"file":null,"fillAlpha":0.15,"fillGraph":false,"fractions":false,"gridLineColor":"#D8D8D8","gridLineWidth":0.3,"hideOverlayOnMouseOut":true,"highlightCircleSize":4,"includeZero":false,"interactionModel":null,"isZoomedIgnoreProgrammaticZoom":false,"labels":null,"labelsDiv":null,"labelsDivStyles":null,"labelsDivWidth":250,"labelsKMB":true,"labelsKMG2":false,"labelsSeparateLines":true,"labelsShowZeroValues":true,"legend":"always","logscale":true,"maxNumberWidth":30,"panEdgeFraction":null,"pixelsPerLabel":null,"pixelsPerXLabel":null,"pixelsPerYLabel":null,"pointSize":1,"rangeSelectorHeight":40,"rangeSelectorPlotFillColor":"#A7B1C4","rangeSelectorPlotStrokeColor":"#808FAB","rightGap":20,"rollPeriod":1,"showLabelsOnHighlight":true,"showRangeSelector":false,"showRoller":false,"sigFigs":null,"sigma":2,"stackedGraph":false,"stepPlot":false,"strokePattern":null,"strokeWidth":4,"ticker":null,"title":null,"titleHeight":18,"valueFormatter":null,"valueRange":null,"visibility":null,"wilsonInterval":true,"xAxisHeight":null,"xAxisLabelFormatter":null,"xAxisLabelWidth":55,"xLabelHeight":18,"xValueFormatter":null,"xValueParser":null,"xlabel":null,"y2label":null,"yAxisLabelFormatter":null,"yAxisLabelWidth":50,"yLabelWidth":18,"yValueFormatter":null,"ylabel":null},"slug":"ohai","name":"Ohai","desc":"","dataset":"/data/datasources/enwp_articles_created.csv","width":"auto","height":320,"chartType":"dygraphs","parents":["root"],"id":"ohai"}
\ No newline at end of file
+{
+    "id": "ohai", 
+    "name": "ohai", 
+    "slug": "ohai", 
+    "desc": "", 
+    "dataset": "/data/datasources/rc/rc_new_article_count.csv", 
+    "width": "auto",
+    "height": 320, 
+    "parents": [
+        "root"
+    ], 
+    "chartType": "dygraphs", 
+    "options": {
+        "animatedZooms": true, 
+        "avoidMinZero": false, 
+        "axis": null, 
+        "axisLabelColor": "#666666", 
+        "axisLabelFontSize": 14, 
+        "axisLabelFormatter": null, 
+        "axisLabelWidth": 50, 
+        "axisLineColor": "#AAAAAA", 
+        "axisLineWidth": 0.3, 
+        "axisTickSize": 3, 
+        "colorSaturation": 1, 
+        "colorValue": 0.5, 
+        "colors": [
+            "#FF0097", 
+            "#EF8158", 
+            "#83BB32", 
+            "#182B53", 
+            "#4596FF", 
+            "#553DC9", 
+            "#AD3238", 
+            "#00FFBC", 
+            "#F1D950"
+        ], 
+        "connectSeparatedPoints": false, 
+        "customBars": false, 
+        "dateWindow": null, 
+        "delimiter": ",", 
+        "digitsAfterDecimal": 2, 
+        "displayAnnotations": false, 
+        "drawPoints": true, 
+        "drawXAxis": true, 
+        "drawXGrid": true, 
+        "drawYAxis": true, 
+        "drawYGrid": true, 
+        "errorBars": false, 
+        "file": null, 
+        "fillAlpha": 0.15, 
+        "fillGraph": false, 
+        "fractions": false, 
+        "gridLineColor": "#D8D8D8", 
+        "gridLineWidth": 0.3, 
+        "hideOverlayOnMouseOut": true, 
+        "highlightCircleSize": 4, 
+        "includeZero": false, 
+        "interactionModel": null, 
+        "isZoomedIgnoreProgrammaticZoom": false, 
+        "labels": null, 
+        "labelsDiv": null, 
+        "labelsDivStyles": null, 
+        "labelsDivWidth": 250, 
+        "labelsKMB": true, 
+        "labelsKMG2": false, 
+        "labelsSeparateLines": true, 
+        "labelsShowZeroValues": true, 
+        "legend": "always", 
+        "logscale": true, 
+        "maxNumberWidth": 30, 
+        "panEdgeFraction": null, 
+        "pixelsPerLabel": null, 
+        "pixelsPerXLabel": null, 
+        "pixelsPerYLabel": null, 
+        "pointSize": 1, 
+        "rangeSelectorHeight": 40, 
+        "rangeSelectorPlotFillColor": "#A7B1C4", 
+        "rangeSelectorPlotStrokeColor": "#808FAB", 
+        "rightGap": 20, 
+        "rollPeriod": 1, 
+        "showLabelsOnHighlight": true, 
+        "showRangeSelector": false, 
+        "showRoller": false, 
+        "sigFigs": null, 
+        "sigma": 2, 
+        "stackedGraph": false, 
+        "stepPlot": false, 
+        "strokePattern": null, 
+        "strokeWidth": 4, 
+        "ticker": null, 
+        "title": null, 
+        "titleHeight": 18, 
+        "valueFormatter": null, 
+        "valueRange": null, 
+        "visibility": null, 
+        "wilsonInterval": true, 
+        "xAxisHeight": null, 
+        "xAxisLabelFormatter": null, 
+        "xAxisLabelWidth": 55, 
+        "xLabelHeight": 18, 
+        "xValueFormatter": null, 
+        "xValueParser": null, 
+        "xlabel": null, 
+        "y2label": null, 
+        "yAxisLabelFormatter": null, 
+        "yAxisLabelWidth": 50, 
+        "yLabelWidth": 18, 
+        "yValueFormatter": null, 
+        "ylabel": null
+    }
+}
index 10c7463..ba4eeb2 100644 (file)
@@ -35,3 +35,9 @@ type        ::=  Use type-specific formatting:
     screen -dr kraken
     ./lib/server/server.co | tee -a logs/kraken.log
 
+
+## Deployer Config
+
+    set -xg KRAKEN_DEPLOY_HOST dsc@reportcard2.pmtpa.wmflabs
+    set -xg KRAKEN_DEPLOY_PATH /srv/reportcard/kraken-ui/
+    set -xg KRAKEN_DEV_HOST master.reportcard.wmflabs.org
index 225ea6e..1a818a3 100644 (file)
@@ -5,26 +5,93 @@ Backbone = require 'backbone'
 
 
 
-/**
- * @class Base model, extending Backbone.Model, used by scaffold and others.
- * @extends Backbone.Model
- */
-BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{
-    ctorName : 'BaseModel'
+BaseBackboneMixin = exports.BaseBackboneMixin =
     
-    # A list of method-names to bind on initialize; set this on a subclass to override.
+    initialize: ->
+        @__apply_bind__()
+    
+    
+    ### Auto-Bound methods
+    
+    /**
+     * A list of method-names to bind on `initialize`; set this on a subclass to override.
+     * @type Array<String>
+     */
     __bind__ : []
     
+    /**
+     * Applies the contents of `__bind__`.
+     */
+    __apply_bind__: ->
+        names = _ @pluckSuperAndSelf '__bind__' .chain().flatten().compact().unique().value()
+        _.bindAll this, ...names if names.length
+    
+    
+    
+    ### Synchronization
+    
+    /**
+     * Count of outstanding tasks.
+     * @type Number
+     */
+    waitingOn : 0
+    
+    
+    /**
+     * Increment the waiting task counter.
+     * @returns {this}
+     */
+    wait: ->
+        count = @waitingOn
+        @waitingOn += 1
+        console.log "#this.wait! #count --> #{@waitingOn}"
+        @trigger('start-waiting', this) if count is 0 and @waitingOn > 0
+        this
+    
+    /**
+     * Decrement the waiting task counter.
+     * @returns {this}
+     */
+    unwait: ->
+        count = @waitingOn
+        @waitingOn -= 1
+        console.warn "#this.unwait! #{@waitingOn} < 0" if @waitingOn < 0
+        console.log "#this.unwait! #count --> #{@waitingOn}"
+        @trigger('stop-waiting', this) if @waitingOn is 0 and count > 0
+        this
+    
+    /**
+     * @param {Function} fn Function to wrap.
+     * @returns {Function} A function wrapping the passed function with a call
+     *  to `unwait()`, then delegating with current context and arguments.
+     */
+    unwaitAnd: (fn) ->
+        self = this
+        ->
+            console.log "#self.unwaitAnd( function #{fn.name or fn.displayName}() )"
+            self.unwait()
+            fn ...
+    
     
+
+mixinBase = exports.mixinBase = (body) ->
+    _.clone(BaseBackboneMixin) import body
+
+
+/**
+ * @class Base model, extending Backbone.Model, used by scaffold and others.
+ * @extends Backbone.Model
+ */
+BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
     
     constructor : function BaseModel
         @__class__      = @constructor
         @__superclass__ = @..__super__.constructor
+        @waitingOn      = 0
         Backbone.Model ...
         @trigger 'create', this
     
-    initialize: ->
-        _.bindAll this, ...@__bind__ if @__bind__.length
+    
     
     
     ### Accessors
@@ -56,6 +123,9 @@ BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{
     #     
     
     
+    
+    
+    
     ### Serialization
     
     serialize: (v) ->
@@ -93,12 +163,11 @@ BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{
     toURL: ->
         "?#{@toKV ...}"
     
-    toString: -> "#{@ctorName}(id=#{@id})"
+    toString: -> "#{@..name or @..displayName}(cid=#{@cid}, id=#{@id})"
 
 
 # Class Methods
 BaseModel import do
-    
     /**
      * Factory method which constructs an instance of this model from a string of KV-pairs.
      * This is a class method inherited by models which extend {BaseModel}.
@@ -117,23 +186,18 @@ BaseModel import do
  * @class Base collection, extending Backbone.Collection, used by scaffold and others.
  * @extends Backbone.Collection
  */
-BaseList = exports.BaseList = Backbone.Collection.extend do # {{{
-    ctorName : 'BaseList'
-    
-    # A list of method-names to bind on initialize; set this on a subclass to override.
-    __bind__ : []
-    
+BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{
     
     
     constructor : function BaseList
         @__class__      = @constructor
         @__superclass__ = @..__super__.constructor
+        @waitingOn      = 0
         Backbone.Collection ...
         @trigger 'create', this
     
-    initialize : ->
-        _.bindAll this, ...@__bind__ if @__bind__.length
     
+    ### Serialization
     
     toKVPairs: ->
         _.collapseObject @toJSON()
@@ -144,7 +208,7 @@ BaseList = exports.BaseList = Backbone.Collection.extend do # {{{
     toURL: (item_delim='&', kv_delim='=') ->
         "?#{@toKV ...}"
     
-    toString: -> "#{@ctorName}(length=#{@length})"
+    toString: -> "#{@..name or @..displayName}(length=#{@length})"
 # }}}
 
 
@@ -152,17 +216,12 @@ BaseList = exports.BaseList = Backbone.Collection.extend do # {{{
  * @class Base view, extending Backbone.View, used by scaffold and others.
  * @extends Backbone.View
  */
-BaseView = exports.BaseView = Backbone.View.extend do # {{{
-    ctorName : 'BaseView'
-    
-    /**
-     * A list of method-names to bind on initialize; set this on a subclass to override.
-     * @type Array<String>
-     */
-    __bind__ : []
+BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
+    tagName : 'section'
     
     /**
-     * @type Array<BaseView>
+     * Array of [view, selector]-pairs.
+     * @type Array<[BaseView, String]>
      */
     subviews : []
     
@@ -171,12 +230,13 @@ BaseView = exports.BaseView = Backbone.View.extend do # {{{
     constructor : function BaseView
         @__class__      = @constructor
         @__superclass__ = @..__super__.constructor
+        @waitingOn      = 0
         @subviews       = []
         Backbone.View ...
         @trigger 'create', this
     
     initialize: ->
-        _.bindAll this, ...@__bind__ if @__bind__.length
+        @__apply_bind__()
         
         @setModel @model
         @build()
@@ -196,24 +256,58 @@ BaseView = exports.BaseView = Backbone.View.extend do # {{{
             @model.on 'destroy', @remove, this
         @model
     
+    
+    
+    ### Subviews
+    
+    addSubview: (selector, view) ->
+        [view, selector] = [selector, null] unless view
+        @subviews.push [view, selector]
+        view
+    
+    removeSubview: (view) ->
+        for [v, sel], idx of @subviews
+            if v is view
+                @subviews.splice(idx, 1)
+                return [v, sel]
+        null
+    
+    hasSubview: (view) ->
+        _.any @subviews, ([v]) -> v is view
+    
+    attachSubviews: ->
+        for [view, selector] of @subviews
+            return unless view
+            view.undelegateEvents()
+            return unless el = view.render()?.el
+            if selector
+                @$el.find selector .append el
+            else
+                @$el.append el
+            view.delegateEvents()
+        this
+    
+    
+    ### Rendering Chain
+    
     toTemplateLocals: ->
         json = {value:v} = @model.toJSON()
         if _.isArray(v) or _.isObject(v)
             json.value = JSON.stringify v
-        { $, _, op, @model, view:this } import json
+        json
     
     $template: (locals={}) ->
-        $ @template @toTemplateLocals() import locals
+        $ @template do
+            { $, _, op, @model, view:this } import @toTemplateLocals() import locals
     
     build: ->
         return this unless @template
-        
         outer = @$template()
         @$el.html outer.html()
             .attr do
                 id    : outer.attr 'id'
                 class : outer.attr('class')
-        
+        @attachSubviews()
         this
     
     render: ->
@@ -221,6 +315,15 @@ BaseView = exports.BaseView = Backbone.View.extend do # {{{
         @trigger 'render', this
         this
     
+    renderSubviews: ->
+        _.invoke _.pluck(@subviews, 0), 'render'
+        this
+    
+    
+    
+    
+    ### UI Utilities
+    
     hide   : -> @$el.hide();      this
     show   : -> @$el.show();      this
     remove : -> @$el.remove();    this
@@ -239,7 +342,7 @@ BaseView = exports.BaseView = Backbone.View.extend do # {{{
     #     @$el.appendTo parent if parent?.length
     #     this
     
-    toString : -> "#{@ctorName}(model=#{@model})"
+    toString : -> "#{@..name or @..displayName}(model=#{@model})"
 
 
 # Proxy model methods
index 7cf2bc7..a531efe 100644 (file)
@@ -44,7 +44,6 @@ KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet()
  * @class Field with chart-option-specific handling for validation, parsing, tags, etc.
  */
 ChartOption = exports.ChartOption = Field.extend do # {{{
-    ctorName : 'ChartOption'
     IGNORED_TAGS : <[ callback deprecated debugging ]>
     
     
@@ -113,7 +112,6 @@ ChartOption = exports.ChartOption = Field.extend do # {{{
  * @class List of ChartOption fields.
  */
 ChartOptionList = exports.ChartOptionList = FieldList.extend do # {{{
-    ctorName   : 'ChartOptionList'
     model      : ChartOption
     
     
index 369c198..ff59a27 100644 (file)
@@ -12,7 +12,6 @@ DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms
  */
 ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{
     # __bind__  : <[ onClick ]>
-    ctorName  : 'ChartOptionView'
     tagName   : 'div'
     className : 'field option'
     template  : require 'kraken/template/chart-option'
@@ -36,18 +35,39 @@ ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{
         @$el.addClass 'collapsed' if @isCollapsed
         this
     
+    
+    /**
+     * Sets the state of `isCollapsed` and updates the UI. If the state changed,
+     * a `'change:collapse`` event will be fired.`
+     * 
+     * @param {Boolean} [makeCollapsed=true] If true, set state to collapsed.
+     * @returns {Boolean} Whether the state changed.
+     */
+    collapse: (state=true) ->
+        state        = !! state
+        @isCollapsed = @$el.hasClass 'collapsed'
+        
+        return this if state is @isCollapsed
+        if state
+            @$el.addClass 'collapsed'
+        else
+            @$el.removeClass 'collapsed'
+        @isCollapsed = state
+        @trigger 'change:collapse', this, @isCollapsed
+        true
+    
+    /**
+     * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event.
+     * @returns {this}
+     */
+    toggleCollapsed: ->
+        @collapse not @$el.hasClass 'collapsed'
+        this
+    
     onClick: (evt) ->
         target = $ evt.target
-        # console.log "#this.onClick()", target
         @toggleCollapsed() if @$el.hasClass('collapsed') and not target.hasClass('close')
     
-    toggleCollapsed: ->
-        starting = @$el.hasClass 'collapsed' #@isCollapsed
-        @$el.toggleClass 'collapsed'
-        @isCollapsed = not starting
-        # console.log "#this.toggleCollapsed!", starting, '->', @isCollapsed
-        @trigger 'change:collapse', this, @isCollapsed
-        this
     
 # }}}
 
@@ -57,16 +77,23 @@ ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{
  * @class View for configuring a chart type.
  */
 ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{
-    ctorName       : 'ChartOptionScaffold'
+    __bind__       : <[ collapseAll expandAll ]>
     tagName        : 'form'
     className      : 'options scaffold'
     template       : require 'kraken/template/chart-scaffold'
+    
     collectionType : ChartOptionList
     subviewType    : ChartOptionView
     fields         : '.fields'
     
+    events:
+        'click .options-filter-button'       : 'onFilterOptions'
+        'click .collapse-all-options-button' : 'collapseAll'
+        'click .expand-all-options-button'   : 'expandAll'
+    
     # GraphView will set this
-    ready          : false
+    ready  : false
+    
     
     
     constructor: function ChartOptionScaffold
@@ -76,8 +103,9 @@ ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{
         @render = _.debounce @render.bind(this), DEBOUNCE_RENDER
         Scaffold::initialize ...
     
+    
     render: ->
-        # console.log "#this.render() -> .isotope()"
+        console.log "#this.render(ready=#{@ready}) -> .isotope()"
         # Scaffold::render ...
         return this unless @ready
         container = if @fields then @$el.find @fields else @$el
@@ -86,13 +114,35 @@ ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{
             .find '.field.option' .addClass 'isotope-item'
         container.isotope do
             # itemPositionDataEnabled : true
-            itemSelector    : '.field.option'
-            layoutMode      : 'masonry'
-            masonry         : columnWidth : 10
-            getSortData :
+            itemSelector : '.field.option'
+            layoutMode   : 'masonry'
+            masonry      : { columnWidth:10 }
+            filter       : @getOptionsFilter()
+            sortBy       : 'category'
+            getSortData  : 
                 category: ($el) ->
                     $el.data 'model' .getCategory()
-            sortBy: 'category'
+        this
+    
+    getOptionsFilter: ->
+        data = @$el.find '.options-filter-button.active' .toArray().map -> $ it .data()
+        sel = data.reduce do
+            (sel, d) ->
+                sel += that if d.filter
+                sel
+            ''
+        sel
+    
+    collapseAll: ->
+        _.invoke @_subviews, 'collapse', true
+        # @renderSubviews()
+        false
+    
+    expandAll: ->
+        _.invoke @_subviews, 'collapse', false
+        # @renderSubviews()
+        false
+    
     
     /**
      * Add a ChartOption to this scaffold, rerendering the isotope
@@ -100,12 +150,17 @@ ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{
      */
     addOne: (field) ->
         view = Scaffold::addOne ...
-        view.on 'change:collapse render', @render
+        view.on 'change:collapse render', @render, this
         view
     
     toKV: ->
         @collection.toKV ...
     
+    
+    onFilterOptions: (evt) ->
+        evt.preventDefault()
+        _.defer @render
+    
 # }}}
 
 
index 75439f9..89d5be6 100644 (file)
@@ -13,12 +13,12 @@ Seq = require 'seq'
  * @class
  */
 DataView = exports.DataView = BaseView.extend do # {{{
-    ctorName       : 'DataView'
+    __bind__       : <[ onReady ]>
     tagName        : 'section'
     className      : 'data-ui'
     template       : require 'kraken/template/data'
     
-    data : {}
+    datasources : {}
     
     
     
@@ -28,36 +28,77 @@ DataView = exports.DataView = BaseView.extend do # {{{
     initialize: ->
         @graph_id = @options.graph_id
         BaseView::initialize ...
-        
+        @on 'ready', @onReady
         @load()
-        
-        # @subviews.push @dataset_view = new DataSetView {@model, @graph_id}
-        # @$el.append @dataset_view.render().el
-        # @dataset_view.on 'edit-metric', @editMetric, this
-        # 
-        # @subviews.push @metric_edit_view  = new MetricEditView  {dataset:@model, @graph_id}
-        # @$el.append @metric_edit_view.render().hide().el
-        
-    
-    toTemplateLocals: ->
-        attrs = _.clone @model.attributes
-        { $, _, op, @model, view:this, @data, @graph_id } import attrs
     
     
     load: ->
-        $.getJSON '/datasources/all', (@data) ~>
+        $.getJSON '/datasources/all', (@datasources) ~>
+            @canonicalizeDataSources @datasources
             @ready = true
             @render()
             @trigger 'ready', this
     
+    /**
+     * Transform the `columns` field to ensure an Array of {label, type} objects.
+     */
+    canonicalizeDataSources: (datasources) ->
+        _.each datasources, (ds) ->
+            ds.shortName    or= ds.name
+            ds.title        or= ds.name
+            ds.subtitle     or= ''
+            
+            cols = ds.columns
+            if _.isArray cols
+                ds.metrics = _.map cols, (col, idx) ->
+                    if _.isArray col
+                        [label, type] = col
+                        {idx, label, type or 'int'}
+                    else
+                        col
+            else
+                ds.metrics = _.map cols.labels, (label, idx) ->
+                    {idx, label, type:cols.types[idx] or 'int'}
+        datasources
+    
+    
+    onReady: ->
+        @metric_edit_view = @addSubview new MetricEditView  {@graph_id, dataset:@model, @datasources}
+        @metric_edit_view
+            .on 'update',           @onUpdateMetric,      this
+        
+        @dataset_view = @addSubview new DataSetView {@model, @graph_id, @dataset, @datasources}
+        @dataset_view
+            .on 'add-metric',       @onMetricsChanged,  this
+            .on 'remove-metric',    @onMetricsChanged,  this
+            .on 'edit-metric',      @editMetric,        this
+        
+        @attachSubviews()
+        this
+    
+    
+    toTemplateLocals: ->
+        attrs = _.clone @model.attributes
+        { $, _, op, @model, view:this, @graph_id, @datasources, } import attrs
+    
+    # attachSubviews: ->
+    #     @$el.empty()
+    #     BaseView::attachSubviews ...
+    #     @$el.append '<div class="clearer"/>'
+    #     this
+    
     # Don't rebuild HTML, simply notify subviews
     render: ->
-        BaseView::render ...
-        
-        # _.invoke @subviews, 'render'
+        @renderSubviews()
+        @trigger 'render', this
         this
     
     editMetric: (metric) ->
         @metric_edit_view.editMetric metric
     
+    onMetricsChanged: ->
+        @$el.css 'min-height', @dataset_view.$el.height()
+    
+    onUpdateMetric: ->
+        @renderSubviews()
 # }}}
index 8aa5262..c5b591d 100644 (file)
@@ -16,7 +16,6 @@ ColorBrewer = require 'colorbrewer'
  * @class
  */
 DataSet = exports.DataSet = BaseModel.extend do # {{{
-    ctorName : 'DataSet'
     urlRoot : '/datasets'
     
     /**
index eadaff0..b98169c 100644 (file)
@@ -8,7 +8,6 @@
  * @class
  */
 DataSetView = exports.DataSetView = BaseView.extend do # {{{
-    ctorName  : 'DataSetView'
     tagName   : 'section'
     className : 'dataset-ui dataset'
     template  : require 'kraken/template/dataset'
@@ -32,6 +31,7 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
     
     
     newMetric: ->
+        console.log "#this.newMetric!"
         # triggers 'add' on @model.metrics
         @model.newMetric()
         false
@@ -39,27 +39,33 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
     addMetric: (metric) ->
         console.log "#this.addMetric!", metric
         if metric.view
-            _.remove @subviews, metric.view
+            @removeSubview metric.view
             delete @views_by_cid[metric.cid]
         
-        @subviews.push view = new DataSetMetricView {model:metric, @graph_id}
+        view = @addSubview new DataSetMetricView {model:metric, @graph_id}
         @views_by_cid[metric.cid] = view
         @$el.find '.metrics' .append view.render().el
         
         # @render()
+        @trigger 'add-metric', metric, view, this
         view
     
     editMetric: (metric) ->
         console.log "#this.editMetric!", metric
         if metric instanceof [jQuery.Event, Event]
             metric = $ metric.currentTarget .data 'model'
-        view = @views_by_cid[metric.cid]
+        view = @active_view = @views_by_cid[metric.cid]
         console.log '  --> metric:', metric, 'view:', view
         
-        @$el.find '.metrics' .removeClass 'metric-active'
+        @$el.find '.metrics .dataset-metric' .removeClass 'metric-active'
         view.$el.addClass 'metric-active'
         view.$el.find '.activity-arrow' .css 'font-size', 2+view.$el.height()
-        @trigger 'edit-metric', metric
+        
+        @trigger 'edit-metric', metric, view, this
+        this
+    
+    render: ->
+        this
     
 # }}}
 
@@ -69,7 +75,6 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
  * @class
  */
 DataSetMetricView = exports.DataSetMetricView = BaseView.extend do # {{{
-    ctorName  : 'DataSetMetricView'
     tagName   : 'tr'
     className : 'dataset-metric metric'
     template  : require 'kraken/template/dataset-metric'
index ef23d98..1d31584 100644 (file)
@@ -8,7 +8,6 @@
  * @class
  */
 DataSource = exports.DataSource = BaseModel.extend do # {{{
-    ctorName : 'DataSource'
     urlRoot  : '/datasources'
     
     
@@ -33,7 +32,6 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{
  * @class
  */
 DataSourceList = exports.DataSourceList = BaseList.extend do # {{{
-    ctorName : 'DataSourceList'
     urlRoot  : '/datasources'
     model    : DataSource
     
index 25d52d1..43b92b9 100644 (file)
@@ -9,24 +9,36 @@
  */
 DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{
     __bind__       : <[  ]>
-    ctorName       : 'DataSourceUIView'
     tagName        : 'section'
     className      : 'datasource-ui'
     template       : require 'kraken/template/datasource-ui'
     
+    events :
+        'click .datasource-summary': 'onHeaderClick'
+    
+    graph_id           : null
+    dataset            : null
+    datasources        : null
+    
+    
     
     constructor: function DataSourceUIView
         BaseView ...
     
     initialize: ->
-        @graph_id = @options.graph_id
+        this import @options.{graph_id, dataset, datasources}
         BaseView::initialize ...
     
     toTemplateLocals: ->
         locals = @model.toJSON()
-        locals import
-            graph_id         : @graph_id
-            source_summary   : 'Source Summary'
-            metric_summary   : 'Metric Summary'
-            timespan_summary : 'Timespan Summary'
+        locals import {
+            @graph_id, @dataset, @datasources,
+            source_summary   : '<Select Source>'
+            metric_summary   : '<Select Metric>'
+            timespan_summary : '<Select Timespan>'
+        }
+    
+    onHeaderClick: ->
+        @$el.toggleClass 'in'
+    
 # }}}
index e7d6aa2..57dc290 100644 (file)
@@ -9,7 +9,6 @@
  */
 DataSourceView = exports.DataSourceView = BaseView.extend do # {{{
     __bind__       : <[  ]>
-    ctorName       : 'DataSourceView'
     tagName        : 'section'
     className      : 'datasource'
     template       : require 'kraken/template/datasource'
index b397ff5..45ca185 100644 (file)
  * @class
  */
 MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
-    ctorName       : 'MetricEditView'
     tagName        : 'section'
     className      : 'metric-edit-ui'
     template       : require 'kraken/template/metric-edit'
     
     graph_id           : null
     dataset            : null
+    datasources        : null
     datasource_ui_view : null
     
     
@@ -27,16 +27,16 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
         BaseView ...
     
     initialize: ->
-        this import @options.{graph_id, dataset}
+        this import @options.{graph_id, dataset, datasources}
         @model or= new Metric
         BaseView::initialize ...
-        @subviews.push @datasource_ui_view = new DataSourceUIView {@model, @graph_id}
+        @datasource_ui_view = @addSubview '.metric-datasource', new DataSourceUIView {@model, @graph_id, @dataset, @datasources}
         @$el.find '.metric-datasource' .append @datasource_ui_view.render().el
     
     
     toTemplateLocals: ->
         locals = BaseView::toTemplateLocals ...
-        locals import {@graph_id}
+        locals import { @graph_id, @dataset, @datasources, }
     
     build: ->
         BaseView::build ...
index 63a88a0..dcaa418 100644 (file)
@@ -8,7 +8,6 @@
  * @class
  */
 Metric = exports.Metric = BaseModel.extend do # {{{
-    ctorName : 'Metric'
     urlRoot  : '/metrics'
     
     /**
@@ -65,7 +64,6 @@ Metric = exports.Metric = BaseModel.extend do # {{{
  * @class
  */
 MetricList = exports.MetricList = BaseList.extend do # {{{
-    ctorName : 'MetricList'
     urlRoot  : '/metrics'
     model    : Metric
     
index 00f3ec2..f67f655 100644 (file)
@@ -199,7 +199,6 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{
     
     
     
-    
     ### Formatters {{{
     
     axisFormatter: (fmttr) ->
index 42329be..07c3935 100644 (file)
@@ -27,12 +27,11 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         valueFormatter xValueFormatter yValueFormatter
     ]>
     __bind__  : <[
-        render renderAll stopAndRender stopAndRenderAll resizeViewport
+        render renderAll stopAndRender stopAndRenderAll resizeViewport wait unwait checkWaiting
         numberFormatter numberFormatterHTML
-        onReady onSync onModelChange onScaffoldChange onDataChange onFirstClickRenderOptionsTab
+        onReady onSync onModelChange onScaffoldChange onFirstClickRenderOptionsTab
     ]>
     __debounce__: <[ render renderAll ]>
-    ctorName  : 'GraphEditView'
     tagName   : 'section'
     className : 'graph graph-edit'
     template  : require 'kraken/template/graph-edit'
@@ -49,39 +48,56 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         'submit   form.options'                    : 'onOptionsSubmit'
         'change   input[type="checkbox"]'          : 'onOptionsSubmit'
     
-    data  : {}
+    
+    /**
+     * Count of outstanding tasks until we stop the spinner.
+     * @type Number
+     */
+    waitingOn : 0
+    
+    /**
+     * Whether we're ready.
+     * @type Boolean
+     */
     ready : false
     
     
+    
     constructor: function GraphEditView
         BaseView ...
     
     initialize : (o={}) ->
-        @data = {}
+        # @data = {}
         @model or= new Graph
-        @id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid)
+        @id = @graph_id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid)
         BaseView::initialize ...
         # console.log "#this.initialize!"
         
         for name of @__debounce__
             @[name] = _.debounce @[name], DEBOUNCE_RENDER
         
-        @viewport = @$el.find '.viewport'
+        # Set up the spinner
+        @on 'start-waiting', @onStartWaiting, this
+        @on 'stop-waiting',  @onStopWaiting,  this
+        @onStartWaiting() if @waitingOn # In case we missed the first call to @wait() somehow
+        
+        # Start a wait for the `ready` event
+        @wait()
         
         ### Model Events
         @model
-            .on 'ready',            @onReady, this
-            .on 'sync',             @onSync,  this
-            .on 'destroy',          @remove,  this
-            .on 'change',           @render,  this
-            .on 'change:dataset',   @onModelChange
-            .on 'change:options',   @onModelChange
-            .on 'error', ~>
-                console.error "#this.error!", arguments
-                # TODO: UI alert
+            .on 'start-waiting',    @wait,          this
+            .on 'stop-waiting',     @unwait,        this
+            .on 'sync',             @onSync,        this
+            .on 'destroy',          @remove,        this
+            .on 'change',           @render,        this
+            .on 'change:dataset',   @onModelChange, this
+            .on 'change:options',   @onModelChange, this
+            .on 'error',            @onModelError,  this
+            .on 'ready',            @onReady,       this
         
         ### Chart Options Tab, Scaffold
-        @scaffold = new ChartOptionScaffold
+        @scaffold = @addSubview '.graph-options-pane', new ChartOptionScaffold
         @$el.find '.graph-options-pane' .append @scaffold.el
         @scaffold.collection.reset that if o.graph_spec
         
@@ -89,13 +105,18 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         @chartOptions @model.getOptions(), {+silent}
         
         # Rerender the options boxes once the tab is visible
+        # Can't use @events because we need to bind before registering
         @$el.on 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab
         
         ### Graph Data UI
-        # @subviews.push @data = new DataView { model:@model.get('dataset'), graph_id:@id }
-        @subviews.push @data = new DataView { @model, graph_id:@id }
+        @data = @addSubview '.graph-data-pane', new DataView { model:@model.get('dataset'), graph_id:@id }
         @$el.find '.graph-data-pane' .append @data.render().el
-        @data.on 'change', @onDataChange
+        @data
+            .on 'change',        @onDataChange, this
+            .on 'start-waiting', @wait,         this
+            .on 'stop-waiting',  @unwait,       this
+        
+        @checkWaiting()
         
         ### Chart Viewport
         @resizeViewport()
@@ -107,15 +128,19 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
     
     
     
+    ### Persistence {{{
+    
     load: ->
         console.log "#this.load!"
-        @model.fetch()
+        @wait()
+        @model.fetch { success:@unwait, error:@unwait }
         false
     
     save: ->
         console.log "#this.save!"
+        @wait()
         id = @model.get('slug') or @model.id
-        @model.save {id}, {+wait}
+        @model.save {id}, { +wait, success:@unwait, error:@unwait }
         false
     
     done: ->
@@ -126,6 +151,10 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         @scaffold.invoke 'change'
         this
     
+    
+    ### }}}
+    ### Rendering {{{
+    
     chartOptions: (values, opts) ->
         # Handle @chartOptions(k, v, opts)
         if arguments.length > 1 and typeof values is 'string'
@@ -145,13 +174,12 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
                     delete options[k]
             options
     
-    
     toTemplateLocals: ->
         attrs = _.clone @model.attributes
         delete attrs.options
         # delete attrs.dataset
-        attrs.data = @data
-        { $, _, op, @model, view:this } import attrs
+        # attrs.data = @data
+        { $, _, op, @model, view:this, @graph_id, slug:'', name:'', desc:'' } import attrs
     
     
     /**
@@ -163,22 +191,24 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         modelH = height = @model.get 'height'
         return { width, height } unless @ready
         
+        viewport = @$el.find '.viewport'
+        
         # Remove old style, as it confuses dygraph after options update
-        @viewport.attr 'style', ''
+        viewport.attr 'style', ''
         label = @$el.find '.graph-label'
         
         if width is 'auto'
-            vpWidth = @viewport.innerWidth()
+            vpWidth = viewport.innerWidth()
             labelW = label.outerWidth()
             width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW)
         width ?= modelW
         if height is 'auto'
-            height = @viewport.innerHeight()
+            height = viewport.innerHeight()
         height ?= modelH
         
         size = { width, height }
-        @viewport.css size
-        # console.log 'resizeViewport!', JSON.stringify(size), @viewport
+        viewport.css size
+        # console.log 'resizeViewport!', JSON.stringify(size), viewport
         # @chart.resize size if forceRedraw
         size
     
@@ -197,11 +227,15 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
                 el.val txt
             
             form.find "textarea[name=#k]" .text txt
+        
+        # Graph Name field is not part of the form due to the layout.
+        @$el.find "input.graph-name[name='name']" .val @get 'name'
         this
     
     # Redraw chart inside viewport.
     renderChart: ->
-        data = @model.get 'dataset' #.getData()
+        data = @model.get 'dataset'
+        data = data.getData() if typeof data is not 'string'
         size = @resizeViewport()
         
         # XXX: use @model.changedAttributes() to calculate what to update
@@ -224,13 +258,13 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         # dygraphs to reset the current option state.
         @chart?.destroy()
         @chart = new Dygraph do
-            @viewport.0
+            @$el.find '.viewport' .0
             data
             options
         
         # unless @chart
         #     @chart = new Dygraph do
-        #         @viewport.0
+        #         @$el.find '.viewport' .0
         #         data
         #         options
         # else
@@ -239,22 +273,33 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         
         this
     
+    attachSubviews: ->
+        @$el.find '.graph-options-pane' .append @scaffold.el        if @scaffold
+        @$el.find '.graph-data-pane'    .append @data.render().el   if @data
+        @checkWaiting()
     
     render: ->
         return this unless @ready
+        @wait()
+        @checkWaiting() # fix up the spinner element as the DOM is now settled
         @renderDetails()
-        _.invoke @subviews, 'render'
+        @attachSubviews()
+        # _.invoke @subviews, 'render'
         @renderChart()
         @updateURL()
         @trigger 'render', this
-        false
+        @unwait()
+        this
     
     renderAll: ->
         return this unless @ready
         # console.log "#this.renderAll!"
+        @wait()
         _.invoke @scaffold.subviews, 'render'
         @scaffold.render()
         @render()
+        @unwait()
+    
     
     /**
      * Update the page URL using HTML5 History API
@@ -267,7 +312,51 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         History.pushState data, title, url
     
     
+    /**
+     * Retrieve or construct the spinner.
+     */
+    spinner: ->
+        el = @$el.find '.graph-spinner'
+        unless el.data 'spinner'
+            ### Spin.js Options ###
+            opts =
+                lines     : 9           # [12]        The number of lines to draw
+                length    : 2           # [7]         The length of each line
+                width     : 1           # [5]         The line thickness
+                radius    : 7           # [10]        The radius of the inner circle
+                rotate    : -10.5       # [0]         rotation offset
+                trail     : 50          # [100]       Afterglow percentage
+                opacity   : 1/4         # [1/4]       Opacity of the lines
+                shadow    : false       # [false]     Whether to render a shadow
+                speed     : 1           # [1]         Spins per second
+                zIndex    : 2e9         # [2e9]       zIndex; uses a very high z-index by default
+                color     : '#000'      # ['#000']    Line color; '#rgb' or '#rrggbb'.
+                top       : 'auto'      # ['auto']    Top position relative to parent in px; 'auto' = center vertically.
+                left      : 'auto'      # ['auto']    Left position relative to parent in px; 'auto' = center horizontally.
+                className : 'spinner'   # ['spinner'] CSS class to assign to the element
+                fps       : 20          # [20]        Frames per second when falling back to `setTimeout()`.
+                hwaccel   : Modernizr.csstransforms3d   # [false]     Whether to use hardware acceleration.
+            
+            isHidden = el.css('display') is 'none'
+            el.show().spin opts
+            el.hide() if isHidden
+        el
+    
+    checkWaiting: ->
+        spinner = @spinner()
+        if isWaiting = (@waitingOn > 0)
+            spinner.show()
+            if spinner.find('.spinner').css('top') is '0px'
+                # delete spinner
+                spinner.spin(false)
+                # re-add to DOM with correct parent sizing
+                @spinner()
+        else
+            spinner.hide()
+        isWaiting
+    
     
+    ### }}}
     ### Formatters {{{
     
     axisFormatter: (fmttr) ->
@@ -298,20 +387,26 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
     numberFormatterHTML: (n, opts, g) ->
         digits = opts('digitsAfterDecimal') ? 2
         { whole, fraction, suffix } = @_numberFormatter n, digits
-        """
-        <span class="value"><span class="whole">#whole</span><span class="fraction">#fraction</span><span class="suffix">#suffix</span></span>
-        """
+        # coco will trim all the whitespace
+        "<span class='value'>
+        <span class='whole'>#whole</span>
+        <span class='fraction'>#fraction</span>
+        <span class='suffix'>#suffix</span>
+        </span>"
     
     ### }}}
     ### Event Handlers {{{
     
-    
     onReady: ->
         return if @ready
-        $.getJSON '/datasources/all', (@data) ~>
-            console.log "(#this via GraphEditView).ready!"
-            @ready = @scaffold.ready = true
-            @onSync()
+        # $.getJSON '/datasources/all', (@data) ~>
+        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
@@ -322,10 +417,25 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         @chartOptions @model.getOptions(), {+silent}
         @renderAll()
     
+    onStartWaiting: ->
+        console.log "#this.onStartWaiting!", @checkWaiting()
+    
+    onStopWaiting: ->
+        console.log "#this.onStopWaiting!", @checkWaiting()
+    
+    onModelError: ->
+        console.error "#this.error!", arguments
+        # TODO: UI alert
+    
     onModelChange: ->
         changes = @model.changedAttributes()
         options = @model.getOptions()
-        # console.log "Graph.changed( options ) ->\n\tchanges: #{JSON.stringify changes}\n\toptions: #{JSON.stringify options}" #"\n\t^opts: #{JSON.stringify _.intersection _.keys(changes), _.keys(options)}"
+        # console.log """
+        #     Graph.changed( options ) ->
+        #     \tchanges: #{JSON.stringify changes}
+        #     \toptions: #{JSON.stringify options}
+        #     \t^opts: #{JSON.stringify _.intersection _.keys(changes), _.keys(options)}
+        # """
         @chart?.updateOptions file:that if changes?.dataset
         @chartOptions options, {+silent} if changes?.options
     
@@ -369,17 +479,11 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         @render()
         false
     
-    # Needed because (sigh) _.debounce returns undefined
-    stopAndRender: ->
-        @render ...
-        false
-    
-    stopAndRenderAll: ->
-        @renderAll ...
-        false
+    # Needed because (sigh) _.debounce returns undefined, and we need to preventDefault()
+    stopAndRender    : -> @render ... ;     false
+    stopAndRenderAll : -> @renderAll ... ;  false
     
     # }}}
     
-    toString: -> "#{@ctorName}(#{@model})"
 # }}}
 
index 87fb3d3..e53f376 100644 (file)
@@ -1,4 +1,4 @@
-Seq = require 'seq'
+Seq = require "seq"
 
 _ = require 'kraken/util/underscore'
 Cascade = require 'kraken/util/cascade'
@@ -18,7 +18,6 @@ root = do -> this
  * other settings for both its content and presentation.
  */
 Graph = exports.Graph = BaseModel.extend do # {{{
-    ctorName       : 'Graph'
     IGNORE_OPTIONS : <[ width height timingName ]>
     urlRoot        : '/graphs'
     
@@ -60,7 +59,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
             name    : ''
             desc    : ''
             notes   : ''
-            dataset : '/data/datasources/rc/rc_comscore_region_uv.csv'
+            # dataset : '/data/datasources/rc/rc_comscore_region_uv.csv'
             # dataset : null
             width   : 'auto'
             height  : 320
@@ -92,7 +91,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         @chartType = ChartType.lookup @get('chartType')
         
         # Insert submodels in place of JSON
-        # @set 'dataset', new DataSet(@get('dataset')), {+silent}
+        @set 'dataset', new DataSet(@get('dataset')), {+silent}
         
         @trigger 'init', this
         @load() if opts.autoload
@@ -100,6 +99,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
     
     load: (opts={}) ->
         return this if @ready and not opts.force
+        @wait()
         @trigger 'load', this
         Seq()
             .seq_ (next) ~>
@@ -107,24 +107,29 @@ Graph = exports.Graph = BaseModel.extend do # {{{
                     next.ok()
                 else
                     console.log "#{this}.fetch()..."
+                    @wait()
                     @fetch do
-                        error : (err) ~>
+                        error : @unwaitAnd (err) ~>
                             console.error "#{this}.fetch() --> error! #arguments"
                             next.ok()
-                        success : (model, res) ~>
+                        success : @unwaitAnd (model, res) ~>
                             # console.log "#{this}.fetch() --> success!", res
                             next.ok res
             .seq_ (next) ~>
                 next.ok @get('parents')
             .flatten()
-            .seqMap -> Graph.lookup it, this
+            .seqMap_ (next, parent_id) ~>
+                @wait()
+                Graph.lookup parent_id, next
             .seqEach_ (next, parent) ~>
                 @parents.add parent
                 @optionCascade.addLookup parent.get('options')
+                @unwait()
                 next.ok()
             .seq ~>
                 @ready = true
                 @trigger 'ready', this
+                @unwait() # terminates the `load` wait
         this
     
     
@@ -209,6 +214,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         options
     
     
+    
     ### Serialization
     
     parse: (data) ->
@@ -271,16 +277,14 @@ Graph = exports.Graph = BaseModel.extend do # {{{
      * @returns {String} URL identifying this model.
      */
     toURL: ->
-        slug = @get 'slug', ''
+        slug = @get('slug') or ''
         slug = "/#slug" if slug
         "#{@urlRoot}#slug?#{@toKV { keepSlug: !!slug }}"
     
-    toString: -> "#{@ctorName}(id=#{@id}, cid=#{@cid})"
 # }}}
 
 
 GraphList = exports.GraphList = BaseList.extend do # {{{
-    ctorName : 'GraphList'
     urlRoot  : '/graphs'
     model    : Graph
     
@@ -291,7 +295,7 @@ GraphList = exports.GraphList = BaseList.extend do # {{{
         modelIds = _.pluck @models, 'id' 
             .map -> "\"#it\""
             .join ', '
-        "#{@ctorName}(#modelIds)"
+        "#{@..name or @..displayName}(#modelIds)"
 # }}}
 
 
index 1d1781f..22de261 100644 (file)
@@ -42,7 +42,8 @@ main = ->
     
     # Extract id from URL
     if match = /\/graphs\/([^\/?]+)/i.exec loc
-        data.id = data.slug = match[1]
+        id = match[1]
+        data.id = data.slug = id unless /(edit|new)/.test id
     
     # _.dump _.clone(data.options), 'data.options'
     
index 61d2da9..e9e431d 100644 (file)
@@ -46,7 +46,8 @@ main = ->
     
     # Extract id from URL
     if match = /\/graphs\/([^\/?]+)/i.exec loc
-        data.id = data.slug = match[1]
+        id = match[1]
+        data.id = data.slug = id unless /(edit|new)/.test id
     
     # _.dump _.clone(data.options), 'data.options'
     
index 05e320d..ee302ab 100644 (file)
@@ -8,7 +8,6 @@ op = require 'kraken/util/op'
 ### Scaffold Models
 
 Field = exports.Field = BaseModel.extend do # {{{
-    ctorName       : 'Field'
     idAttribute    : 'name'
     valueAttribute : 'value'
     
@@ -107,7 +106,6 @@ Field = exports.Field = BaseModel.extend do # {{{
 
 
 FieldList = exports.FieldList = BaseList.extend do # {{{
-    ctorName : 'FieldList'
     model    : Field
     
     
@@ -135,6 +133,5 @@ FieldList = exports.FieldList = BaseList.extend do # {{{
     toURL: (item_delim='&', kv_delim='=') ->
         "?#{@toKV ...}"
     
-    toString: -> "#{@ctorName}(length=#{@length})"
 # }}}
 
index be8e36a..1b74bdb 100644 (file)
@@ -70,11 +70,12 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
     
     
     constructor: function Scaffold
+        @_