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