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 ...
-        
-        # _.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
+        @_subviews = []
         BaseView ...
     
     initialize: ->
         CollectionType = @collectionType
-        @model = @collection or= new CollectionType
+        @model = (@collection or= new CollectionType)
         BaseView::initialize ...
         
         @collection.on 'add',   @addOne
@@ -83,9 +84,13 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
         @$el.data { model:@collection, view:this } .addClass @className
     
     
+    renderSubviews: ->
+        _.invoke @_subviews, 'render'
+        this
+    
     addOne: (field) ->
         # console.log "[S] #this.addOne!", @..__super__
-        _.remove @subviews, field.view if field.view
+        _.remove @_subviews, field.view if field.view
         
         # avoid duplicating event propagation
         field.off 'change:value', @change, this
@@ -95,7 +100,7 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
         
         SubviewType = @subviewType
         view = new SubviewType model:field
-        @subviews.push view
+        @_subviews.push view
         container = if @fields then @$el.find @fields else @$el
         container.append view.render().el unless field.get 'ignore'
         view.on 'update', @change.bind(this, field)
@@ -104,8 +109,8 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
         view
     
     addAll: ->
-        # _.invoke @subviews, 'remove'
-        # @subviews = []
+        _.invoke @_subviews, 'remove'
+        @_subviews = []
         @collection.each @addOne
         this
     
@@ -117,7 +122,6 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
         this
     
     
-    toString: -> "#{@ctorName}(collection=#{@collection})"
 
 
 
index ba4d6a6..d1ea72c 100644 (file)
@@ -1,3 +1,13 @@
-form.options.scaffold
+form.graph-options.scaffold.form-inline
+    
+    .graph-options-controls.control-group
+        a.collapse-all-options-button.btn(href="#") Collapse All
+        a.expand-all-options-button.btn(href="#") Expand All
+        
+        .std-adv-filter-buttons.btn-group.pull-right(data-toggle="buttons-radio")
+            a.standard-filter-button.options-filter-button.btn.active(href="#", data-filter=".tag_standard") Standard
+            a.advanced-filter-button.options-filter-button.btn(href="#", data-filter="") Advanced
+        
+    
     .fields.control-group
 
index 241affd..e51e63b 100644 (file)
@@ -1,14 +1,16 @@
 section.data-ui
-    .row-fluid
-        .dataset-controls.dropdown
-            select(name="dataset")
-                for datum, k in data
-                    - var selected = (datum.url === dataset ? 'selected' : null);
-                    option(value=datum.url, name=datum.id, selected=selected) #{datum.name}
-                
-            //- 
-                a.span3.dataset.dropdown-toggle(data-toggle="dropdown", data-target="#{graph_id} .dataset-dropdown", href="#dataset-dropdown") Select...
-                ul.dropdown-menu
-                    for datum, k in data
-                        li: a(href="#", data-dataset="#{datum.id}") #{datum.name}
-    
+    //- 
+        .row-fluid
+        //- 
+            .dataset-controls.dropdown
+                select(name="dataset")
+                    for datum, k in datasources
+                        - var selected = (datum.url === dataset ? 'selected' : null);
+                        option(value=datum.url, name=datum.id, selected=selected) #{datum.name}
+        
+        //- 
+            a.span3.dataset.dropdown-toggle(data-toggle="dropdown", data-target="#{graph_id} .dataset-dropdown", href="#dataset-dropdown") Select...
+            ul.dropdown-menu
+                for datum, k in datasources
+                    li: a(href="#", data-dataset="#{datum.id}") #{datum.name}
+
index 89b5bb7..1eb4e62 100644 (file)
@@ -1,26 +1,25 @@
-section.dataset-ui.dataset
-    .inner
-        h4 Graph Data Set
-        
-        .dataset-buttons
-            a.new-metric-button.btn.btn-success.btn-small(href="#")
-                i.icon-plus-sign.icon-white
-                |  Add Metric
-            //- 
-                a.clear-metrics-button.btn.btn-danger.btn-small(href="#")
-                    i.icon-remove-sign.icon-white
-                    |  Remove All Metrics
-        
-        .dataset-metrics
-            table.table.table-striped
-                thead
-                    tr
-                        th.col-label    Label
-                        th.col-source   Source
-                        th.col-times    Timespan
-                        th.col-actions  Actions
-                tbody.metrics
-                    //- DataSetMetricViews attach here
-                
-            
+section.dataset-ui.dataset: div.inner
+    h4 Graph Data Set
+    
+    .dataset-buttons
+        a.new-metric-button.btn.btn-success.btn-small(href="#")
+            i.icon-plus-sign.icon-white
+            |  Add Metric
+        //- 
+            a.clear-metrics-button.btn.btn-danger.btn-small(href="#")
+                i.icon-remove-sign.icon-white
+                |  Remove All Metrics
+    
+    .dataset-metrics
+        table.table.table-striped
+            thead
+                tr
+                    th.col-label    Label
+                    th.col-source   Source
+                    th.col-times    Timespan
+                    th.col-actions  Actions
+            tbody.metrics
+                //- DataSetMetricViews attach here
+    
+
 
index 870c864..2f53006 100644 (file)
@@ -1,23 +1,43 @@
 section.datasource-ui
     
     section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui .datasource-selector")
+        i.expand-datasource-ui-button.icon-chevron-down
+        i.collapse-datasource-ui-button.icon-chevron-up
+        
         ul.breadcrumb
-            li #{source_summary}
-                span.divider
-            li #{metric_summary}
-                span.divider
+            li #{source_summary} 
+                span.divider /
+            li #{metric_summary} 
+                span.divider /
             li #{timespan_summary}
-        
-        i.icon-chevron-down.pull-right
-        i.icon-chevron-up.pull-right
     
-    section.datasource-selector.collapse.in
-        .tabbable.tabs-left
-            ul.datasource-sources-list.nav.nav-tabs
-                li: h4 Data Sources
-                //- li: a(href="#", data-toggle="tab", data-target="")
-            .datasource-sources-details.tab-content
-                .datasource-source.tab
-                    .datasource-source-details
-                    .datasource-source-time
-                    .datasource-source-metrics
+    section.datasource-selector.collapse
+        .datasource-tabs
+            .tabbable.tabs-left
+                ul.datasource-sources-list.nav.nav-tabs
+                    li: h6 Data Sources
+                    for ds, k in datasources
+                        - var ds_target = "#"+graph_id+" .datasource-ui .datasource-selector .datasource-source.datasource-source-"+ds.id
+                        li: a(href="#datasource-selector_datasource-source-#{ds.id}", data-toggle="tab", data-target=ds_target) #{ds.shortName}
+                
+                .datasource-sources-details.tab-content
+                    for ds, k in datasources
+                        .datasource-source.tab-pane(class="datasource-source-#{ds.id}")
+                            .datasource-source-details
+                                .source-name #{ds.name}
+                                .source-id #{ds.id}
+                                .source-url #{ds.url}
+                                .source-format #{ds.format}
+                                .source-charttype #{ds.chart.chartType}
+                            .datasource-source-time
+                                .source-time-start #{ds.timespan.start}
+                                .source-time-end #{ds.timespan.end}
+                                .source-time-step #{ds.timespan.step}
+                            ol.datasource-source-metrics
+                                for m, idx in ds.metrics.slice(1)
+                                    li.datasource-source-metric
+                                        span.source-metric-idx #{m.idx}
+                                        |  
+                                        span.source-metric-label #{m.label}
+                                        |  
+                                        span.source-metric-type #{m.type}
index 8fb078e..dae7c39 100644 (file)
@@ -1,6 +1,6 @@
 include browser-helpers
 - var graph_id = view.id
-section.graph.graph-display(id=view.id)
+section.graph.graph-display(id=graph_id)
     
     .graph-name-row.page-header.row-fluid
         h2.graph-name #{name}
index ba1ae30..580e728 100644 (file)
@@ -1,50 +1,55 @@
 - var graph_id = view.id || model.id || model.cid
 section.graph.graph-edit(id=graph_id)
-    form.details.form-horizontal
-        
-        .name-row.row-fluid.control-group
-            //- label.name.control-label(for="#{id}_name"): h3 Graph Name
-            input.span6.name(type='text', id="#{graph_id}_name", name="name", placeholder='Graph Name', value=name)
-        
-        .row-fluid
-            .viewport
-            .graph-label
-        
-        .row-fluid
-            .graph-settings.tabbable
-                //- nav.navbar: div.navbar-inner: div.container
-                nav
-                    .graph-controls.pull-right
-                        .btn-group
-                            a.redraw-button.btn(href="#")
-                                i.icon-refresh
-                                |  Redraw
-                        .btn-group
-                            a.load-button.btn(href="#")
-                                i.icon-download
-                                |  Revert
-                            a.save-button.btn(href="#")
-                                i.icon-upload
-                                |  Save
-                            a.done-button.btn-primary.btn(href="#")
-                                i.icon-ok-sign.icon-white
-                                |  Done
-                    ul.nav.subnav.nav-pills
-                        li:         h3 Graph
-                        li.active:  a(href="##{graph_id}-tab-info", data-toggle="tab") Info
-                        li:         a(href="##{graph_id}-tab-data", data-toggle="tab") Data
-                        li: a.graph-options-tab(href="##{graph_id}-tab-options", data-toggle="tab") Options
-                    
+    
+    //- Graph Name field is not part of the form due to the layout.
+    .graph-name-row.row-fluid.control-group
+        //- label.name.control-label(for="#{id}_name"): h3 Graph Name
+        input.span6.graph-name(type='text', id="#{graph_id}_name", name="name", placeholder='Graph Name', value=name)
+    
+    .graph-viewport-row.row-fluid
+        .viewport
+        .graph-label
+    
+    .graph-settings-row.row-fluid
+        .graph-settings.tabbable
+            
+            //--- Main Tabs ---//
+            nav.graph-settings-nav
+                .graph-controls.pull-right
+                    .btn-group
+                        a.redraw-button.btn(href="#")
+                            i.icon-refresh
+                            |  Redraw
+                    .btn-group
+                        a.load-button.btn(href="#")
+                            i.icon-download
+                            |  Revert
+                        a.save-button.btn(href="#")
+                            i.icon-upload
+                            |  Save
+                        a.done-button.btn-primary.btn(href="#")
+                            i.icon-ok-sign.icon-white
+                            |  Done
+                ul.nav.subnav.nav-pills
+                    li
+                        .graph-spinner
+                        h3 Graph
+                    li.active:  a(href="##{graph_id}-tab-info", data-toggle="tab") Info
+                    li:         a(href="##{graph_id}-tab-data", data-toggle="tab") Data
+                    li: a.graph-options-tab(href="##{graph_id}-tab-options", data-toggle="tab") Options
                 
-                .tab-content
-                    .graph-info-pane.tab-pane.active(id="#{graph_id}-tab-info")
+            
+            //--- Tab Panes ---//
+            .graph-tab-content.tab-content
+                .graph-info-pane.tab-pane.active(id="#{graph_id}-tab-info")
+                    form.graph-details.form-horizontal
                         .row-fluid
                             .half.control-group
                                 .control-group
                                     label.slug.control-label(for="#{graph_id}_slug") Slug
                                     .controls
                                         input.span3.slug(type='text', id="#{graph_id}_slug", name='slug', placeholder='graph_slug', value=slug)
-                                        p.help-block The slug uniquely identifies this graph and will be displayed in the URL.
+                                        p.help-block The slug uniquely identifies this graph and will be displayed in the URL once saved.
                                 .control-group
                                     label.width.control-label(for="#{graph_id}_width") Size
                                     .controls
@@ -58,13 +63,13 @@ section.graph.graph-edit(id=graph_id)
                                     //- textarea.span3.desc(id='desc', name='desc', placeholder='Graph description.') #{desc}
                                     <textarea class="span3 desc" id="#{graph_id}_desc" name="desc" placeholder="Graph description.">#{desc}</textarea>
                                     p.help-block A description of the graph.
-                    
-                    .graph-data-pane.tab-pane(id="#{graph_id}-tab-data")
+                
+                .graph-data-pane.tab-pane(id="#{graph_id}-tab-data")
+                    //- 
                         .row-fluid
-                            //- 
-                                label.dataset.control-label(for="#{graph_id}_dataset") Data Set
-                                input.span3.dataset(type='text', id="#{graph_id}_dataset", name='dataset', placeholder='URL to dataset file', value=dataset)
-                                p.help-block This dataset filename will soon be replaced by a friendly UI.
-                    
-                    .graph-options-pane.tab-pane(id="#{graph_id}-tab-options")
+                            label.dataset.control-label(for="#{graph_id}_dataset") Data Set
+                            input.span3.dataset(type='text', id="#{graph_id}_dataset", name='dataset', placeholder='URL to dataset file', value=dataset)
+                            p.help-block This dataset filename will soon be replaced by a friendly UI.
+                
+                .graph-options-pane.tab-pane(id="#{graph_id}-tab-options")
 
index f48442a..ffe0a7f 100644 (file)
@@ -1,14 +1,14 @@
 section.metric-edit-ui
     .inner: form.form-horizontal
         
-        .metric-header.control-group.row-fluid
-            .color-picker
+        .metric-header.control-group
+            .color-swatch(style="background-color: #{color};")
             input.metric-color(type='hidden', id="#{graph_id}_metric", name="color", value=color)
             input.metric-label(type='text', id="#{graph_id}_metric_label", name='label', placeholder='Metric Label', value=label)
         
-        .metric-datasource.row-fluid
+        .metric-datasource.control-group
         
-        .metric-actions.row-fluid
+        .metric-actions.control-group
             a.delete-button.btn.btn-danger(href="#")
                 i.icon-remove.icon-white
                 |  Delete
index d867674..4ecfb14 100644 (file)
@@ -1,3 +1,4 @@
+### Patches to make Backbone work with browserify
 
 # Expose Underscore so Backbone can find it
 _ = require 'underscore'
@@ -11,22 +12,55 @@ window?.Backbone = Backbone
 Backbone.setDomLibrary that if window? and (window.jQuery or window.Zepto or window.ender)
 
 
+
+
+/**
+ * @namespace Meta-utilities for working with Backbone classes.
+ */
 _backbone = do
     
     /**
      * @returns {Array<Class>} The list of all superclasses for this class or object.
      */
     getSuperClasses: function getSuperClasses(Cls)
-        Cls .= constructor if cls and typeof Cls is not 'function'
-        if superclass = Cls?.__super__?.constructor
+        return [] unless Cls
+        
+        if Cls.__superclass__
+            superclass = that
+        else
+            Cls = Cls.constructor unless typeof Cls is 'function'
+            superclass = Cls.__super__?.constructor
+        
+        if superclass
             [superclass].concat getSuperClasses superclass
         else
             []
     
+    /**
+     * Looks up an attribute on the prototype of each class in the class
+     * hierarchy.
+     * @returns {Array}
+     */
+    pluckSuper: (obj, prop) ->
+        return [] unless obj
+        _ _backbone.getSuperClasses(obj) .chain()
+        .pluck 'prototype'
+        .pluck prop 
+        .value()
+    
+    /**
+     * As `.pluckSuper()` but includes value of `prop` on passed `obj`.
+     * @returns {Array}
+     */
+    pluckSuperAndSelf: (obj, prop) ->
+        return [] unless obj
+        [ obj[prop] ].concat _backbone.pluckSuper(obj, prop)
+
 
 exports import _backbone
 
 
+
 /**
  * Decorates a function so that its receiver (`this`) is always added as the
  * first argument, followed by the call arguments.
index e971d11..9ab1258 100644 (file)
@@ -5,6 +5,10 @@
 
 
 section.graph section.data-ui
+    height 100%
+    min-height 300px
+    clearfix()
+    
     h4
         display none
     
@@ -13,7 +17,10 @@ section.graph section.data-ui
     
     /* * * *  DataSet UI  * * * */
     section.dataset-ui
+        absolute 0 0
         width 40%
+        height 100%
+        min-height 300px
         border-right 1px solid #ccc
         
         .inner
@@ -93,13 +100,102 @@ section.graph section.data-ui
     
     /* * * *  Edit Metric UI * * * */
     section.metric-edit-ui
-        position absolute
-        top 0
-        left 40%
+        display none
+        float right
+        width 60%
+        min-height 100%
         
         .inner
             padding 1em
         
+        .metric-header
+            min-height 38px
+            font-size 18px
+            line-height 25px
+            
+            .color-swatch
+                absolute top 3px right 0
+                width 30px
+                height 30px
+                border 1px solid #333
+                border-color #ddd #333 #333 #ddd
+                border-radius 3px
+                cursor pointer
+            
+            input
+                display block
+                absolute top 0 left 0
+                right 38px
+                width auto
+                font-size 18px
+                line-height 25px
+                height 25px
+                padding 6px
         
     
+    /* * * *  DataSource UI  * * * */
+    section.datasource-ui
+        
+        // Collapse/Expand icon
+        i
+            z-index 100
+        .expand-datasource-ui-button
+            display block
+        .collapse-datasource-ui-button
+            display none
+        &.in
+            .expand-datasource-ui-button
+                display none
+            .collapse-datasource-ui-button
+                display block
+        
+        // Summary Header
+        .datasource-summary
+            &, &:hover
+                cursor pointer
+            i
+                absolute top 50% right 14px
+                margin-top -7px
+            .breadcrumb
+                margin-bottom 0
+                font-weight bold
+            // &, .breadcrumb
+            //     color #3a87ad
+            //     background-color #d9edf7
+            //     border-color #bce8f1
+            //     background-image linear-gradient(top, #d9edf7, darken(#d9edf7, 10%))
+        
+        // Selector UI (with tabs per-source, info, etc)
+        .datasource-selector
+            .datasource-tabs
+                top -1px
+                padding 7px
+                border 1px solid #ddd
+                border-top 0
+                border-radius 3px
+            
+            h1, h2, h3, h4, h5, h6
+                margin 0 -1px 3px 0
+                padding 8px 12px
+                min-width 74px
+            
+            .datasource-sources-list
+                height 100%
+                display table-cell
+                float none
+                max-width 200px
+            
+            .datasource-sources-details
+                display table-cell
+                padding 1em
+                width auto
+            
+            
+        
+    
+    
+    .data-ui
+        clearfix()
+    
+    
 
index 627748c..ce12b87 100644 (file)
@@ -3,13 +3,29 @@
 
 section.graph
     position relative
-    max-width 900px
     margin 0 auto
+    min-width 640px
+    max-width 960px
     
     *
         position relative
     
+    .graph-details > *
+        margin-left auto
+        margin-right auto
+    
+    
     /* * * *  Chart & Viewport  * * * {{{ */
+    .graph-viewport-row
+        // margin 1em 1em 3em
+        margin-bottom 3em
+    
+    .viewport
+        position relative
+        min-width 200px
+        min-height 320px
+        overflow hidden
+    
     .graph-label
         position absolute
         z-index 100
@@ -18,9 +34,10 @@ section.graph
         width 200px
         
         padding 1em
-        border-radius 5px
         background-color rgba(255,255,255, 0.75)
         font 12px/1.5 "helvetica neue", helvetica, arial, sans-serif
+        border 1px solid $light
+        border-radius 5px
         
         b
             display inline-block
@@ -37,19 +54,44 @@ section.graph
     .viewport:hover + .graph-label
         border 1px solid $light
     
-    .viewport
-        position relative
-        min-width 200px
-        min-height 320px
-        margin-bottom 1.5em
-        overflow hidden
+    /* }}} */
+    
     
+    /* * * *  Graph Details & Info Pane  * * * {{{ */
+    .graph-name-row
+        // margin 0 1em 1em
+        // max-width 900px
+        font-size 120%
+        line-height 2em
+        input.graph-name
+            font-size 120%
+            line-height 1.2
+            height 1.2em
+            border-color $light
+            width 98%
+    .graph-info-pane
+        .row-fluid
+            .half.control-group
+                width 50%
+                float left
+                margin-left 0
+                margin-right 0
+            label
+                width 100px
+            .controls
+                margin-left 110px
+        .help-block
+            font-size 11px
+            line-height 1.3
     /* }}} */
     
     
     /* * * *  Subnav & Tabs  * * * {{{ */
+    .graph-settings-row
+        max-width 900px
+    
     .graph-settings.tabbable
-        .nav
+        .graph-settings-nav > .nav
             margin-bottom 0
             
             li h3
@@ -60,13 +102,15 @@ section.graph
             li
                 margin-right 4px
         
-        .tab-pane
+        .graph-tab-content > .tab-pane
             padding 0.5em
-            margin-top 18px
+            margin-top 1em
+            height 100%
             
             &.graph-data-pane
                 margin-top 0
                 padding 0
+                border-bottom 1px solid #ddd
     
     .graph-controls
         z-index 100
@@ -82,41 +126,32 @@ section.graph
             min-width 5em
             text-align center
     
-    /* }}} */
-    
+    .graph-spinner
+        display none
+        absolute bottom 3px left 0
+        margin-left -2.0em
+        width  2.5em
+        height 2.5em
     
-    /* * * *  Graph Details  * * * {{{ */
-    form.details
-        position relative
-        
-        .name-row
-            font-size 120%
-            line-height 2em
-            input.name
-                font-size 120%
-                line-height 1.2
-                height 1.2em
-                border-color $light
-                width 98%
-        .row-fluid
-            .half.control-group
-                width 50%
-                float left
-                margin-left 0
-                margin-right 0
-            label
-                width 100px
-            .controls
-                margin-left 110px
-        .help-block
-            font-size 11px
-            line-height 1.3
     /* }}} */
     
     
     /* * * *  Chart Options  * * * {{{ */
-    .options fieldset
-        border 0px
+    .graph-options
+        
+        .graph-options-controls
+            font-size 11px
+            
+            .control-group, .btn-group
+                display inline-block
+                font-size 11px
+            
+            .btn
+                font-size 11px
+                padding 3px 9px 3px
+            & > .btn, & :not(.btn-group) .btn
+                margin-right 0.75em
+            
     
     .field.option
         float left
index 048da3f..f9d775a 100644 (file)
@@ -21,7 +21,7 @@ header, footer, #content
     & > .inner
         position relative
         margin 0 auto
-        max-width 960px
+        // max-width 960px
         
         &.fluid-inner
             width 80%
index c217b2d..93bb9d9 100644 (file)
@@ -18,9 +18,8 @@ dev:
         - jquery.history.min
         - jquery.hotkeys.min
         - jquery.isotope.min
+        - jquery.spin.min
         - bootstrap.min
-        # - spin.min
-        # - jquery.spin.min
         
         ### CommonJS Support Starts Here:
         ### Browserify must come before any .mod files