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=