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)
30 files changed:
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 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 ...
-        
-    &n