Checkpoint on Data UI.
authordsc <dsc@wikimedia.org>
Mon, 16 Apr 2012 20:08:18 +0000 (13:08 -0700)
committerdsc <dsc@wikimedia.org>
Mon, 16 Apr 2012 20:08:18 +0000 (13:08 -0700)
lib/base/base-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/metric-edit-view.co
lib/dataset/metric-model.co
lib/main-edit.co
lib/template/datasource-ui.jade
www/css/data.styl

index 8172727..7d22832 100644 (file)
@@ -15,16 +15,32 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
     tagName : 'section'
     
     /**
+     * The identifier for this view class.
+     * By default, the class name is converted to underscore-case, and a
+     * trailing '_view' suffix is dropped.
+     * Example: "CamelCaseView" becomes "camel_case"
+     * @type String
+     */
+    __view_type_id__: null
+    
+    /**
      * Array of [view, selector]-pairs.
      * @type Array<[BaseView, String]>
      */
     subviews : []
     
+    /**
+     * Whether this view has been added to the DOM.
+     * @type Boolean
+     */
+    _parented: false
+    
     
     
     constructor : function BaseView
         @__class__      = @constructor
         @__superclass__ = @..__super__.constructor
+        @__view_type_id__ or= _.str.underscored @getClassName() .replace /_view$/, ''
         @waitingOn      = 0
         @subviews       = []
         Backbone.View ...
@@ -90,6 +106,10 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
         _.pluck @subviews, 0 .forEach @removeSubview, this
         this
     
+    bubbleEvent: (evt) ->
+        @invokeSubviews 'trigger', ...arguments
+        this
+    
     
     ### Rendering Chain
     
@@ -123,14 +143,32 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
         this
     
     
+    attach: (el) ->
+        @$el.appendTo el
+        # only trigger the event the first time
+        return this if @_parented
+        @_parented = true
+        _.delay do
+            ~> # have to let DOM settle to ensure elements can be found
+                @delegateEvents()
+                @trigger 'parent', this
+            50
+        this
+    
+    remove : ->
+        @undelegateEvents()
+        @$el.remove()
+        return this unless @_parented
+        @_parented = false
+        @trigger 'unparent', this
+        this
     
     
     ### UI Utilities
     
-    hide   : -> @$el.hide();      this
-    show   : -> @$el.show();      this
-    remove : -> @$el.remove();    this
-    clear  : -> @model.destroy(); @remove()
+    hide   : -> @$el.hide();      @trigger('hide', this);       this
+    show   : -> @$el.show();      @trigger('show', this);       this
+    clear  : -> @model.destroy(); @trigger('clear', this);      @remove()
     
     
     # remove : ->
index 298b6d2..d031f97 100644 (file)
@@ -18,7 +18,8 @@ DataView = exports.DataView = BaseView.extend do # {{{
     className      : 'data-ui'
     template       : require 'kraken/template/data'
     
-    datasources : {}
+    data        : {}
+    datasources : null
     
     
     
@@ -28,6 +29,7 @@ DataView = exports.DataView = BaseView.extend do # {{{
     initialize: ->
         @graph_id = @options.graph_id
         BaseView::initialize ...
+        @datasources = @model.sources
         @on 'ready', @onReady, this
         @load()
     
@@ -50,8 +52,9 @@ DataView = exports.DataView = BaseView.extend do # {{{
     
     load: ->
         @wait()
-        $.getJSON '/datasources/all', (@datasources) ~>
-            _.each @datasources, @canonicalizeDataSource, this
+        $.getJSON '/datasources/all', (@data) ~>
+            _.each @data, @canonicalizeDataSource, this
+            @model.sources.reset _.map @data, -> it
             @ready = true
             @unwait()
             @render()
@@ -81,7 +84,7 @@ DataView = exports.DataView = BaseView.extend do # {{{
     
     toTemplateLocals: ->
         attrs = _.clone @model.attributes
-        { $, _, op, @model, view:this, @graph_id, @datasources, } import attrs
+        { $, _, op, @model, view:this, @graph_id, @datasources } import attrs
     
     # attachSubviews: ->
     #     @$el.empty()
index fc8983c..0cef0ad 100644 (file)
@@ -29,6 +29,11 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
      */
     metrics : null
     
+    defaults : ->
+        palette : null
+        lines   : []
+        metrics : []
+    
     
     constructor: function DataSet
         BaseModel ...
@@ -36,24 +41,21 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
     initialize : ->
         BaseModel::initialize ...
         @sources = new DataSourceList
-        mx = @attributes.metrics or= {}
-        @metrics = new MetricList mx.columns
+        @metrics = new MetricList @attributes.metrics
         @on 'change:metrics', @onMetricChange, this
     
     
-    defaults : ->
-        palette : null
-        lines   : []
-        metrics :
-            defaults : {}
-            columns  : []
-    
     
     load: (opts={}) ->
         return this if @ready and not opts.force
         @wait()
         @trigger 'load', this
-        Seq()
+        Seq @metrics.pluck 'source_id'
+            .parMap_ (next, source_id) ->
+                DataSource.lookup source_id, next
+            .seqEach_ (next, source) ~>
+                @sources.add source
+                next.ok source
             .seq ~>
                 @ready = true
                 @trigger 'ready', this
@@ -61,6 +63,10 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
         this
     
     
+    # TODO: toJSON() must ensure columns in MetricList are ordered by index
+    #   ...in theory, MetricList.comparator now does this
+    
+    
     /**
      * @returns {Array} The reified dataset, materialized to an array of data-series arrays.
      */
@@ -74,10 +80,7 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
     
     
     onMetricChange: ->
-        mx = @get 'metrics'
-        cols = mx.columns.map (col) ->
-            _.clone(col) import mx.defaults
-        @metrics.reset cols
+        @metrics.reset @get 'metrics'
     
 # }}}
 
index aeb6910..efb7d0d 100644 (file)
@@ -28,9 +28,10 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
         BaseView::initialize ...
         @views_by_cid = {}
         @addAllMetrics()
-        @model.metrics.on 'add',    @addMetric,         this
-        @model.metrics.on 'remove', @removeMetric,      this
-        @model.metrics.on 'reset',  @addAllMetrics,     this
+        @model.metrics
+            .on 'add',    @addMetric,         this
+            .on 'remove', @removeMetric,      this
+            .on 'reset',  @addAllMetrics,     this
     
     
     newMetric: ->
index 61a9660..7d3ef82 100644 (file)
@@ -8,14 +8,28 @@
  * @class
  */
 DataSource = exports.DataSource = BaseModel.extend do # {{{
+    __bind__ : <[ onLoadSuccess onLoadError ]>
     urlRoot  : '/datasources'
+    ready    : false
+    
+    /**
+     * Parsed data for this datasource.
+     * @type Array
+     */
+    data : null
+    
+    
+    
     
     
     constructor: function DataSource
         BaseModel ...
     
     initialize: ->
+        @attributes = @canonicalize @attributes
         BaseModel::initialize ...
+        @constructor.register this
+        @on 'load-success', @onLoadSuccess, this
     
     
     defaults: ->
@@ -32,7 +46,7 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{
         
         timespan : 
             start     : null
-            stop      : null
+            end       : null
             step      : '1mo'
         
         columns       : []
@@ -44,10 +58,83 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{
     url: ->
         "/datasources/#{@id}.json"
     
+    canonicalize: (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.type or= 'int'
+                    col
+        else
+            ds.metrics = _.map cols.labels, (label, idx) ->
+                {idx, label, type:cols.types[idx] or 'int'}
+        ds
+    
+    load: ->
+        @trigger 'load', this
+        url = @get 'url'
+        switch @get 'format'
+        case 'json'
+            @loadJSON url
+        case 'csv'
+            @loadCSV url
+        default
+            console.error "#this.load() Unknown Data Format!"
+            @trigger 'load-error', this, 'Unknown Data Format!'
+        this
+    
+    onLoadSuccess: ->
+        return if @ready
+        @ready = true
+        @trigger 'ready', this
+    
+    onLoadError: (jqXHR, txtStatus, err) ->
+        @_loadError = true
+        console.error "#this Error loading data! -- #msg: #{err or ''}"
+    
+    
+    loadJSON: (url) ->
+        $.ajax do
+            url      : url
+            dataType : 'json'
+            success : (@data) ->
+                @trigger 'load-success', this
+            error : (jqXHR, txtStatus, err) ->
+                @trigger 'load-error', this, txtStatus, err
+        this
+    
+    loadCSV: (url) ->
+        $.ajax do
+            url      : url
+            dataType : 'text'
+            success : (@data) ->
+                @trigger 'load-success', this
+            error : (jqXHR, txtStatus, err) ->
+                @trigger 'load-error', this, txtStatus, err
+        this
+    
+    parseCSV: (data) ->
+        ...
+    
+    
+    getColumnName: (idx) ->
+        @get('metrics')?[idx]?.label
+    
+    getColumnIndex: (name) ->
+        return that.idx if _.find @get('metrics'), -> it.label is name
+        -1
+    
+    
 # }}}
 
 
-
 /**
  * @class
  */
@@ -57,15 +144,16 @@ DataSourceList = exports.DataSourceList = BaseList.extend do # {{{
     
     constructor: function DataSourceList then BaseList ...
     initialize : -> BaseList::initialize ...
-    
-    
 # }}}
 
 
+
+
 /* * * *  DataSource Cache  * * * */
 
 DataSource import do
     CACHE : new DataSourceList
+    ready : false
     
     register: (model) ->
         # console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model
@@ -77,16 +165,30 @@ DataSource import do
     get: (id) ->
         @CACHE.get id
     
-    lookup: (id, cb) ->
+    lookup: (id, cb, cxt=this) ->
         # console.log "#{@CACHE}.lookup(#id, #{typeof cb})"
+        unless @ready
+            @on 'cache-ready', ~>
+                @off 'cache-ready', arguments.callee
+                @lookup id, cb, cxt
+            return
+        
         if @CACHE.get id
-            cb null, that
+            cb.call cxt, null, that
         else
             Cls = this
             @register new Cls {id}
-                .on 'ready', -> cb null, it
+                .on 'ready', -> cb.call cxt, null, it
     
 
 _.bindAll DataSource, 'register', 'get', 'lookup'
 
 
+# Fetch all DataSources
+$.getJSON '/datasources/all', (data) ->
+    DataSource.CACHE.reset _.map data, -> it
+    DataSource.ready = true
+    DataSource.trigger 'cache-ready', DataSource
+
+
+
index 43b92b9..affc7d5 100644 (file)
@@ -31,12 +31,21 @@ DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{
     
     toTemplateLocals: ->
         locals = @model.toJSON()
-        locals import {
-            @graph_id, @dataset, @datasources,
-            source_summary   : '<Select Source>'
-            metric_summary   : '<Select Metric>'
-            timespan_summary : '<Select Timespan>'
-        }
+        locals import {@graph_id, @dataset, @datasources}
+        
+        ds = @model.source
+        hasSource = @model.get('source_id')? and ds
+        locals.source_summary   = unless hasSource then '<Select Source>' else ds.get 'shortName'
+        
+        hasMetric = hasSource and @model.get('source_col')?
+        locals.metric_summary   = unless hasMetric then '<Select Metric>' else @model.getSourceColumnName()
+        
+        dsts = ds?.get('timespan') or {}
+        ts = locals.timespan = _.defaults _.clone(@model.get('timespan')), dsts
+        hasTimespan = hasMetric and ts.start and ts.end and ts.step
+        locals.timespan_summary = unless hasTimespan then '<Select Timespan>' else "#{ts.start} &mdash; #{ts.end}"
+        
+        locals
     
     onHeaderClick: ->
         @$el.toggleClass 'in'
index 45ca185..b8f743b 100644 (file)
@@ -36,7 +36,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
     
     toTemplateLocals: ->
         locals = BaseView::toTemplateLocals ...
-        locals import { @graph_id, @dataset, @datasources, }
+        locals import { @graph_id, @dataset, @datasources }
     
     build: ->
         BaseView::build ...
@@ -46,7 +46,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
     
     editMetric: (metric) ->
         console.log "#this.editMetric!", metric
-        @model = metric
+        @datasource_ui_view.model = @model = metric
         @render()
         @show()
         this
index b3630bd..8d22de8 100644 (file)
@@ -2,6 +2,8 @@
 } = require 'kraken/util'
 { BaseModel, BaseList,
 } = require 'kraken/base'
+{ DataSource, DataSourceList,
+} = require 'kraken/dataset/datasource-model'
 
 
 /**
@@ -16,25 +18,16 @@ Metric = exports.Metric = BaseModel.extend do # {{{
      */
     source : null
     
-    
-    constructor: function Metric
-        BaseModel ...
-    
-    initialize : ->
-        BaseModel::initialize ...
-    
-    
     defaults : ->
         index           : 0
         label           : 'New Metric'
         type            : 'int'
-        timespan        : { start:null, stop:null, step:null }
+        timespan        : { start:null, end:null, step:null }
         disabled        : false
         
         # DataSource
-        source_id       : null
-        source_col_idx  : -1
-        source_col_name : ''
+        source_id  : null
+        source_col : -1
         
         # Chart Options
         color           : null
@@ -42,8 +35,37 @@ Metric = exports.Metric = BaseModel.extend do # {{{
         format_value    : null
         format_axis     : null
         
-        scale           : 1.0
         transforms      : []
+        scale           : 1.0
+    
+    
+    
+    constructor: function Metric
+        BaseModel ...
+    
+    initialize : ->
+        BaseModel::initialize ...
+        @on 'change:source_id', @onUpdateSource, this
+        @onUpdateSource()
+    
+    
+    onUpdateSource: ->
+        if source_id = @get 'source_id'
+            @wait()
+            DataSource.lookup source_id, @onSourceReady, this
+        this
+    
+    onSourceReady: (err, source) ->
+        console.log "#this.onSourceReady", arguments
+        @unwait()
+        if err
+            console.error "#this Error loading DataSource! #err"
+        else
+            @source = source
+            unless @ready
+                @ready = true
+                @trigger 'ready', this
+        this
     
     
     /**
@@ -52,10 +74,11 @@ Metric = exports.Metric = BaseModel.extend do # {{{
      */
     isOk: ->
         (label = @get('label')) and label is not 'New Metric'
-        and @get('source_id')   and @get('source_col_idx') >= 0
+        and @get('source_id')   and @get('source_col') >= 0
         and _.every @get('timespan'), op.ok
     
-    
+    getSourceColumnName: ->
+        @source?.getColumnName @get 'source_col'
     
 # }}}
 
@@ -70,4 +93,7 @@ MetricList = exports.MetricList = BaseList.extend do # {{{
     constructor: function MetricList then BaseList ...
     initialize: -> BaseList::initialize ...
     
+    comparator: (metric) ->
+        metric.get('index') ? Infinity
+    
 # }}}
index e9e431d..2bd6679 100644 (file)
@@ -11,6 +11,8 @@ Backbone = require 'backbone'
   ChartOption, ChartOptionList, TagSet,
   ChartOptionView, ChartOptionScaffold,
 } = require 'kraken/chart'
+{ DataSource, DataSourceList,
+} = require 'kraken/dataset'
 { Graph, GraphList, GraphEditView,
 } = require 'kraken/graph'
 
@@ -60,14 +62,12 @@ main = ->
     $ '#content .inner' .append view.el
 
 
-
 # Load data files
 Seq([   <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>
 ])
 .parEach_ (next, [key, url]) ->
     jQuery.ajax do
         url : url,
-        dataType : 'json'
         success : (res) ->
             root[key] = res
             next.ok()
index 2f53006..83be803 100644 (file)
@@ -16,28 +16,41 @@ section.datasource-ui
             .tabbable.tabs-left
                 ul.datasource-sources-list.nav.nav-tabs
                     li: h6 Data Sources
-                    for ds, k in datasources
+                    for source, k in datasources.models
+                        - var ds = source.attributes
+                        - var activeClass = (source_id === ds.id ? 'active' : '')
                         - 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}
+                        li(class=activeClass): 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}
+                    for source, k in datasources.models
+                        - var ds = source.attributes
+                        - var activeClass = (source_id === ds.id ? 'active' : '')
+                        .datasource-source.tab-pane(class="datasource-source-#{ds.id} #{activeClass}")
+                            .well
+                                .datasource-source-details
+                                    .source-name #{ds.name}
+                                    .source-id #{ds.id}
+                                    .source-format #{ds.format}
+                                    .source-charttype #{ds.chart.chartType}
+                                    input.source-url(type="text", name="source-url", value=ds.url)
+                                .datasource-source-time
+                                    .source-time-start #{ds.timespan.start}
+                                    .source-time-end #{ds.timespan.end}
+                                    .source-time-step #{ds.timespan.step}
+                            .datasource-source-metrics
+                                h6 Metrics
+                                table.table.table-striped
+                                    thead
+                                        tr
+                                            th.source-metric-idx #
+                                            th.source-metric-label Label
+                                            th.source-metric-type Type
+                                    tbody.source-metrics
+                                        for m, idx in ds.metrics.slice(1)
+                                            - var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+                                            tr.datasource-source-metric(class=activeColClass)
+                                                td.source-metric-idx #{m.idx}
+                                                td.source-metric-label #{m.label}
+                                                td.source-metric-type #{m.type}
+                                            
index a3c4212..7a83db6 100644 (file)
@@ -186,15 +186,39 @@ section.graph section.data-ui
                 height 100%
                 display table-cell
                 float none
-                // width 200px
-                // min-width 200px
                 li a
                     min-width 150px
             
+            .well
+                padding 0.75em
+            
             .datasource-sources-details
                 display table-cell
                 padding 1em
-                // width auto
+                .source-id, .source-format, .source-charttype,
+                thead, .source-metric-idx, .source-metric-type
+                    display none
+                .source-name
+                    font-size 15px
+                    margin-bottom 0.75em
+                .source-url
+                    display block
+                    width 95%
+                    font-family menlo, monospace
+                .datasource-source-time
+                    margin 0.5em
+                    & > *
+                        display inline-block
+                        margin-right 1em
+                .datasource-source-metric
+                    &, &:hover
+                        cursor pointer
+                    &.active td
+                        color #3a87ad
+                        background-color #d9edf7
+                        border 1px solid #3a87ad
+                        border-width 1px 0
+