Refactors much of GraphEditView and GraphDisplayView into a common base.
authordsc <dsc@wikimedia.org>
Wed, 16 May 2012 07:28:12 +0000 (00:28 -0700)
committerdsc <dsc@wikimedia.org>
Wed, 16 May 2012 07:28:12 +0000 (00:28 -0700)
lib/base/base-view.co
lib/graph/graph-display-view.co
lib/graph/graph-edit-view.co
lib/graph/graph-view.co [new file with mode: 0644]
lib/graph/index.co
lib/template/graph-edit.jade
www/css/graph.styl
www/modules.yaml

index dc9f0d0..a8d3ec9 100644 (file)
@@ -209,12 +209,14 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
         this
     
     render: ->
+        @wait()
         if @isBuilt
             @update()
         else
             @build()
         @renderSubviews()
         @trigger 'render', this
+        @unwait()
         this
     
     renderSubviews: ->
index 9ccc38e..eb34859 100644 (file)
@@ -2,32 +2,19 @@ moment = require 'moment'
 
 { _, op,
 } = require 'kraken/util'
-{ BaseView,
-} = require 'kraken/base'
 { Graph,
 } = require 'kraken/graph/graph-model'
+{ GraphView,
+} = require 'kraken/graph/graph-view'
 
 root = do -> this
-DEBOUNCE_RENDER = 100ms
 
 
 
 /**
  * @class View for a graph visualization encapsulating.
  */
-GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{
-    FILTER_CHART_OPTIONS : <[ 
-        file labels visibility colors dateWindow ticker timingName xValueParser
-        axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter
-        valueFormatter xValueFormatter yValueFormatter
-    ]>
-    __bind__  : <[
-        render stopAndRender resizeViewport
-        numberFormatter numberFormatterHTML
-        onReady onSync onModelChange 
-    ]>
-    __debounce__: <[ render ]>
-    
+GraphDisplayView = exports.GraphDisplayView = GraphView.extend do # {{{
     tagName   : 'section'
     className : 'graph graph-display'
     template  : require 'kraken/template/graph-display'
@@ -38,8 +25,6 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{
         'click      .export-button'         : 'exportChart'
         # 'click      .load-button'           : 'load'
     
-    data  : {}
-    ready : false
     
     
     constructor: function GraphDisplayView
@@ -47,238 +32,57 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{
     
     initialize : (o={}) ->
         @data = {}
-        @model or= new Graph
-        @id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid)
-        BaseView::initialize ...
+        GraphDisplayView.__super__.initialize ...
         # console.log "#this.initialize!"
         
-        for name of @__debounce__
-            @[name] = _.debounce @[name], DEBOUNCE_RENDER
-        
-        
-        ### 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
-        
         @chartOptions @model.getOptions(), {+silent}
-        
-        ### Chart Viewport
-        @resizeViewport()
-        
-        # Resize chart on window resize
-        # Note: can't debounce the method itself, as the debounce wrapper returns undefined
-        $ root .on 'resize', _.debounce @resizeViewport, DEBOUNCE_RENDER
-        
-        _.delay (~> @onReady()), 100 if @model.ready
+        @loadData()
     
+    onReady: ->
+        return if @ready
+        @triggerReady()
+        @onSync()
     
     
     
-    load: ->
-        console.log "#this.load!"
-        @model.fetch()
-        false
+    ### Rendering {{{
     
-    change: ->
-        @model.change()
+    update: ->
+        locals = @toTemplateLocals()
+        @$ '.graph-name a' .text(locals.name or '')
+        @$ '.graph-desc' .html jade.filters.markdown locals.desc or ''
         this
     
-    chartOptions: (values, opts) ->
-        # Handle @chartOptions(k, v, opts)
-        if arguments.length > 1 and typeof values is 'string'
-            [k, v, opts] = arguments
-            values = { "#k": v }
-        
-        options = @model.getOptions {-keepDefaults, +keepUnchanged}
-        for k of @FILTER_CHART_OPTIONS
-            # console.log "filter #k?", not options[k]
-            if k in options and not options[k]
-                delete options[k]
-        options
-    
-    
-    toTemplateLocals: ->
-        attrs = _.clone @model.attributes
-        delete attrs.options
-        # delete attrs.dataset
-        attrs.data = @data
-        { $, _, op, @model, view:this } import attrs
-    
-    
-    /**
-     * Resizes chart according to the model's width and height.
-     * @return { width, height }
-     */
-    resizeViewport: ->
-        modelW = width  = @model.get 'width'
-        modelH = height = @model.get 'height'
-        return { width, height } unless @ready
-        
-        # Remove old style, as it confuses dygraph after options update
-        viewport = @$ '.viewport'
-        viewport.attr 'style', ''
-        label = @$ '.graph-legend'
-        
-        if width is 'auto'
-            vpWidth = viewport.innerWidth() or 300
-            labelW = label.outerWidth() or 228
-            width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW)
-        width ?= modelW
-        if height is 'auto'
-            height = viewport.innerHeight() or 320
-        height ?= modelH
-        
-        size = { width, height }
-        viewport.css size
-        # console.log 'resizeViewport!', JSON.stringify(size), viewport
-        # @chart.resize size if forceRedraw
-        size
-    
-    
-    # Redraw chart inside viewport.
-    renderChart: ->
-        data = @model.get 'dataset' #.getData()
-        size = @resizeViewport()
-        viewport = @$ '.viewport'
-        
-        # XXX: use @model.changedAttributes() to calculate what to update
-        options = @chartOptions() #import size
-        options import do
-            labelsDiv          : @$ '.graph-legend' .0
-            valueFormatter     : @numberFormatterHTML
-            axes:
-                x:
-                    axisLabelFormatter : @axisDateFormatter
-                    valueFormatter     : @dateFormatter
-                y:
-                    axisLabelFormatter : @axisFormatter @numberFormatter
-                    valueFormatter     : @numberFormatterHTML
-        
-        # console.log "#this.render!", dataset
-        # _.dump options, 'options'
-
-        
-        # Always rerender the chart to sidestep the case where we need to push defaults into
-        # dygraphs to reset the current option state.
-        @chart?.destroy()
-        @chart = new Dygraph do
-            viewport.0
-            data
-            options
-        
-        # unless @chart
-        #     @chart = new Dygraph do
-        #         viewport.0
-        #         data
-        #         options
-        # else
-        #     @chart.updateOptions options
-        #     @chart.resize size
-        
+    render: ->
+        return this unless @ready and not @isRendering
+        @wait()
+        @checkWaiting()
+        root.title = "#{@get 'name'} | GraphKit"
+        GraphDisplayView.__super__.render ...
+        @unwait()
+        @checkWaiting()
+        @isRendering = false
         this
-
     
     /**
      * Exports graph as png
      */
     exportChart: (evt) ->
         # The following code is dygraph specific, thus should not 
-        # be implemented in this class. Rather in a dygraph charttype
-        # related class. The same is true for the 'renderChart' method above.
-
+        # be implemented in this class. Rather in the Dygraphs ChartType-subclass. 
+        # The same is true for the 'renderChart' method above.
+        # 
         # The Dygraph.Export module is from http://cavorite.com/labs/js/dygraphs-export/
-        # Todo: We don't use the tile of the chart, which is thus missing from the png
+        # TODO: We don't use the title of the chart, which is thus missing from the png.
         console.log "#this.export!"
         img = @$el.find '.export-image'
-        Dygraph.Export.asPNG(@chart, img);
+        Dygraph.Export.asPNG @chart, img
         window.open img.src, "toDataURL() image"
-
-    
-    
-    update: ->
-        locals = @toTemplateLocals()
-        @$ '.graph-name a' .text(locals.name or '')
-        @$ '.graph-desc' .html jade.filters.markdown locals.desc or ''
-        this
-    
-    render: ->
-        return this unless @ready
-        root.title = "#{@get 'name'} | GraphKit"
-        @update()
-        _.invoke @subviews, 'render'
-        @renderChart()
-        @trigger 'render', this
-        false
-    
-    
     
-    ### Formatters {{{
-    
-    axisFormatter: (fmttr) ->
-        (n, granularity, opts, g) -> fmttr n, opts, g
-    
-    axisDateFormatter: (n, granularity, opts, g) ->
-        moment(n).format 'MM/YYYY'
-    
-    dateFormatter: (n, opts, g) ->
-        moment(n).format 'DD MMM YYYY'
-    
-    _numberFormatter: (n, digits=2) ->
-        for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]]
-            if n >= d
-                n = n / d
-                break
-        s = n.toFixed(digits)
-        parts = s.split '.'
-        whole = _.rchop parts[0], 3 .join ','
-        fraction = '.' + parts.slice(1).join '.'
-        { n, digits, whole, fraction, suffix }
-    
-    numberFormatter: (n, opts, g) ->
-        digits = opts('digitsAfterDecimal') ? 2
-        { whole, fraction, suffix } = @_numberFormatter n, digits
-        "#whole#fraction#suffix"
-    
-    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>
-        """
     
     ### }}}
     ### Event Handlers {{{
     
-    
-    onReady: ->
-        return if @ready
-        $.getJSON '/datasources/all', (@data) ~>
-            console.log "(#this via GraphDisplayView).ready!"
-            @ready = true
-            @onSync()
-    
-    onSync: ->
-        return unless @ready
-        console.info "#this.sync() --> success!"
-        # TODO: UI alert
-        # @change()
-        # @model.change()
-        @render()
-    
-    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)}"
-        @chart?.updateOptions file:that if changes?.dataset
-    
     /**
      * Selects the graph permalink input field.
      */
@@ -288,14 +92,6 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{
         # defer, the focusing click will 
         # unselect the text.
         _.defer( ~> @$ '.graph-permalink input' .select() )
-
-
-
-    # Needed because (sigh) _.debounce returns undefined
-    stopAndRender: ->
-        @render ...
-        false
-    
     
     # }}}
 # }}}
index 37b44a8..05bbbac 100644 (file)
@@ -1,12 +1,12 @@
 moment = require 'moment'
 
 _ = require 'kraken/util/underscore'
-{ BaseView,
-} = require 'kraken/base'
-{ ChartOptionScaffold, DEBOUNCE_RENDER,
-} = require 'kraken/chart'
 { Graph,
 } = require 'kraken/graph/graph-model'
+{ GraphView,
+} = require 'kraken/graph/graph-view'
+{ ChartOptionScaffold, DEBOUNCE_RENDER,
+} = require 'kraken/chart'
 { DataView, DataSetView, DataSet,
 } = require 'kraken/dataset'
 
@@ -20,20 +20,8 @@ root = do -> this
  * - Graph metadata, such as name, description, slug
  * - Chart options, using ChartOptionScaffold
  */
-GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
-    FILTER_CHART_OPTIONS : <[ 
-        file labels visibility colors dateWindow ticker timingName xValueParser
-        axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter
-        valueFormatter xValueFormatter yValueFormatter
-    ]>
-    __bind__  : <[
-        render stopAndRender resizeViewport wait unwait checkWaiting
-        numberFormatter numberFormatterHTML
-        onReady onSync onModelChange onScaffoldChange 
-        onFirstClickRenderOptionsTab onFirstClickRenderDataTab
-    ]>
-    __debounce__: <[ render ]>
-    tagName   : 'section'
+GraphEditView = exports.GraphEditView = GraphView.extend do # {{{
+    __bind__  : <[ wait unwait onScaffoldChange onFirstClickRenderOptionsTab onFirstClickRenderDataTab ]>
     className : 'graph-edit graph'
     template  : require 'kraken/template/graph-edit'
     
@@ -50,17 +38,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         'change   .chart-options input[type="checkbox"]' : 'onOptionsSubmit'
     
     
-    /**
-     * Count of outstanding tasks until we stop the spinner.
-     * @type Number
-     */
-    waitingOn : 0
-    
-    /**
-     * Whether we're ready.
-     * @type Boolean
-     */
-    ready : false
     
     
     
@@ -68,34 +45,12 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         BaseView ...
     
     initialize : (o={}) ->
-        @model or= new Graph
-        @id = @graph_id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid)
-        BaseView::initialize ...
+        GraphEditView.__super__.initialize ...
         # console.log "#this.initialize!"
         
-        for name of @__debounce__
-            @[name] = _.debounce @[name], DEBOUNCE_RENDER
-        
-        # 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 '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 = @addSubview new ChartOptionScaffold
         @scaffold.collection.reset that if o.graph_spec
@@ -110,28 +65,14 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
             .on 'stop-waiting',  @unwait,       this
             .on 'metric-change', @onDataChange, this
         
-        
         # Rerender once the tab is visible
         # Can't use @events because we need to bind before registering
         @$el.on 'click', '.graph-data-tab',     @onFirstClickRenderDataTab
         @$el.on 'click', '.graph-options-tab',  @onFirstClickRenderOptionsTab
         
         
-        ### Chart Viewport
-        @resizeViewport()
-        
-        # Resize chart on window resize
-        # Note: can't debounce the method itself, as the debounce wrapper returns undefined
-        $ root .on 'resize', _.debounce @resizeViewport, DEBOUNCE_RENDER
-        
         # Kick off model load chain
-        @checkWaiting()
-        Seq()
-            .seq_ (next) ~>
-                @model.once 'ready', next.ok .load()
-            .seq_ (next) ~>
-                @model.once 'data-ready', next.ok .loadData()
-            .seq ~> @onReady()
+        @loadData()
     
     onReady: ->
         return if @ready
@@ -151,22 +92,15 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
     
     ### Persistence {{{
     
-    load: ->
-        console.log "#this.load!"
-        @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, success:@unwait, error:@unwait }
-        false
-    
+    /**
+     * Save the graph and return to the graph viewer/browser.
+     */
     done: ->
         @save()
     
+    /**
+     * Flush all changes.
+     */
     change: ->
         @model.change()
         @scaffold.invoke 'change'
@@ -195,125 +129,20 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
                     delete options[k]
             options
     
-    toTemplateLocals: ->
-        attrs = _.clone @model.attributes
-        delete attrs.options
-        { @model, view:this, @graph_id, slug:'', name:'', desc:'' } import attrs
-    
-    
-    /**
-     * Resizes chart according to the model's width and height.
-     * @return { width, height }
-     */
-    resizeViewport: ->
-        modelW = width  = @model.get 'width'
-        modelH = height = @model.get 'height'
-        return { width, height } unless @ready
-        
-        viewport = @$ '.viewport'
-        
-        # Remove old style, as it confuses dygraph after options update
-        viewport.attr 'style', ''
-        label = @$ '.graph-label'
-        
-        if width is 'auto'
-            vpWidth = viewport.innerWidth() or 300
-            labelW = label.outerWidth() or 228
-            width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW)
-        width ?= modelW
-        if height is 'auto'
-            height = viewport.innerHeight() or 320
-        height ?= modelH
-        
-        size = { width, height }
-        viewport.css size
-        # console.log 'resizeViewport!', JSON.stringify(size), viewport
-        # @chart.resize size if forceRedraw
-        size
-    
-    
-    # Repopulate UI from Model
-    renderDetails: ->
-        form = @$ 'form.graph-details'
-        for k, v in @model.attributes
-            continue if k is 'options'
-            txt = @model.serialize v
-            
-            el = form.find "input[name=#k]"
-            if el.attr('type') is 'checkbox'
-                el.attr 'checked', if v then 'checked' else ''
-            else
-                el.val txt
-            
-            form.find "textarea[name=#k]" .text txt
-        
-        # Graph Name field is not part of the form due to the layout.
-        @$ "input.graph-name[name='name']" .val @get 'name'
-        this
-    
-    # Redraw chart inside viewport.
-    renderChart: ->
-        # data = @model.get 'dataset'
-        # data = data.getData() if typeof data is not 'string'
-        dataset = @model.dataset
-        data = dataset.getData()
-        size = @resizeViewport()
-        
-        # XXX: use @model.changedAttributes() to calculate what to update
-        options = @chartOptions() #import size
-        options import do
-            colors             : dataset.getColors()
-            labels             : dataset.getLabels()
-            labelsDiv          : @$ '.graph-label' .0
-            valueFormatter     : @numberFormatterHTML
-            axes:
-                x:
-                    axisLabelFormatter : @axisDateFormatter
-                    valueFormatter     : @dateFormatter
-                y:
-                    axisLabelFormatter : @axisFormatter @numberFormatter
-                    valueFormatter     : @numberFormatterHTML
-        
-        # console.log "#this.render!", dataset
-        # _.dump options, 'options'
-        
-        # Always rerender the chart to sidestep the case where we need to push defaults into
-        # dygraphs to reset the current option state.
-        @chart?.destroy()
-        @chart = new Dygraph do
-            @$ '.viewport' .0
-            data
-            options
-        
-        # unless @chart
-        #     @chart = new Dygraph do
-        #         @$ '.viewport' .0
-        #         data
-        #         options
-        # else
-        #     @chart.updateOptions options
-        #     @chart.resize size
-        
-        this
-    
     attachSubviews: ->
-        BaseView::attachSubviews ...
+        GraphEditView.__super__.attachSubviews ...
         @checkWaiting()
     
     render: ->
-        return this unless @ready and not @_rendering
-        @_rendering = true
+        return this unless @ready and not @isRendering
         @wait()
         @checkWaiting()
-        # @renderDetails()
-        # @attachSubviews()
-        # _.invoke @subviews, 'render'
+        root.title = "#{@get 'name'} | GraphKit"
         GraphEditView.__super__.render ...
         @renderChart()
         # @updateURL()
-        @trigger 'render', this
         @unwait()
-        @_rendering = false
+        @isRendering = false
         this
     
     
@@ -329,77 +158,8 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
     
     
     ### }}}
-    ### Formatters {{{
-    
-    axisFormatter: (fmttr) ->
-        (n, granularity, opts, g) -> fmttr n, opts, g
-    
-    axisDateFormatter: (n, granularity, opts, g) ->
-        moment(n).format 'MM/YYYY'
-    
-    dateFormatter: (n, opts, g) ->
-        moment(n).format 'DD MMM YYYY'
-    
-    _numberFormatter: (n, digits=2) ->
-        for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]]
-            if n >= d
-                n = n / d
-                break
-        s = n.toFixed(digits)
-        parts = s.split '.'
-        whole = _.rchop parts[0], 3 .join ','
-        fraction = '.' + parts.slice(1).join '.'
-        { n, digits, whole, fraction, suffix }
-    
-    numberFormatter: (n, opts, g) ->
-        digits = opts('digitsAfterDecimal') ? 2
-        { whole, fraction, suffix } = @_numberFormatter n, digits
-        "#whole#fraction#suffix"
-    
-    numberFormatterHTML: (n, opts, g) ->
-        digits = opts('digitsAfterDecimal') ? 2
-        { whole, fraction, suffix } = @_numberFormatter n, digits
-        # 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 {{{
     
-    onSync: ->
-        return unless @ready
-        console.info "#this.sync() --> success!"
-        # TODO: UI alert
-        @chartOptions @model.getOptions(), {+silent}
-        @render()
-    
-    onStartWaiting: ->
-        status = @checkWaiting()
-        # console.log "#this.onStartWaiting!", status
-    
-    onStopWaiting: ->
-        status = @checkWaiting()
-        # console.log "#this.onStopWaiting!", status
-    
-    onModelError: ->
-        console.error "#this.error!", arguments
-        # TODO: UI alert
-    
-    onModelChange: ->
-        changes = @model.changedAttributes()
-        options = @model.getOptions()
-        # 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
-    
     onScaffoldChange: (scaffold, value, key, field) ->
         current = @model.getOption(key)
         # console.log do
@@ -444,55 +204,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         @render()
         false
     
-    # Needed because (sigh) _.debounce returns undefined, and we need to preventDefault()
-    stopAndRender    : -> @render ... ;     false
-    
-    # }}}
-    ### Spinner {{{
-    
-    /**
-     * Retrieve or construct the spinner.
-     */
-    spinner: ->
-        el = @$ '.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
-    
     # }}}
     
 # }}}
diff --git a/lib/graph/graph-view.co b/lib/graph/graph-view.co
new file mode 100644 (file)
index 0000000..4ff25d3
--- /dev/null
@@ -0,0 +1,353 @@
+moment = require 'moment'
+
+_ = require 'kraken/util/underscore'
+{ BaseView,
+} = require 'kraken/base'
+{ Graph,
+} = require 'kraken/graph/graph-model'
+
+root = do -> this
+DEBOUNCE_RENDER = 100ms
+
+
+
+
+/**
+ * @class Base view for a Graph visualizations.
+ */
+GraphView = exports.GraphView = BaseView.extend do # {{{
+    FILTER_CHART_OPTIONS : <[ 
+        file labels visibility colors dateWindow ticker timingName xValueParser
+        axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter
+        valueFormatter xValueFormatter yValueFormatter
+    ]>
+    __bind__  : <[
+        render stopAndRender resizeViewport checkWaiting
+        numberFormatter numberFormatterHTML
+        onReady onSync onModelChange
+    ]>
+    __debounce__: <[ render ]>
+    tagName   : 'section'
+    
+    
+    constructor: function GraphView
+        BaseView ...
+    
+    initialize : (o={}) ->
+        @model or= new Graph
+        @id = @graph_id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid)
+        GraphView.__super__.initialize ...
+        
+        for name of @__debounce__
+            @[name] = _.debounce @[name], DEBOUNCE_RENDER
+        
+        # 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
+        
+        ### Model Events
+        @model
+            .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
+        
+        ### Chart Viewport
+        @resizeViewport()
+        
+        # Resize chart on window resize
+        # Note: can't debounce the method itself, as the debounce wrapper returns undefined
+        $ root .on 'resize', _.debounce @resizeViewport, DEBOUNCE_RENDER
+    
+    loadData: ->
+        @resizeViewport()
+        @wait()
+        Seq()
+            .seq_ (next) ~>
+                @model.once 'ready', next.ok .load()
+            .seq_ (next) ~>
+                @model.once 'data-ready', next.ok .loadData()
+            .seq ~>
+                @unwait()
+                @onReady()
+    
+    
+    ### Persistence {{{
+    
+    /**
+     * Reload the graph definition from the server.
+     */
+    load: ->
+        console.log "#this.load!"
+        @wait()
+        @model.fetch { success:@unwait, error:@unwait }
+        false
+    
+    /**
+     * Save the graph definition to the server.
+     */
+    save: ->
+        console.log "#this.save!"
+        @wait()
+        id = @model.get('slug') or @model.id
+        @model.save {id}, { +wait, success:@unwait, error:@unwait }
+        false
+    
+    /**
+     * Flush all changes.
+     */
+    change: ->
+        @model.change()
+        this
+    
+    
+    ### Rendering {{{
+    
+    chartOptions: (values, opts) ->
+        # Handle @chartOptions(k, v, opts)
+        if arguments.length > 1 and typeof values is 'string'
+            [k, v, opts] = arguments
+            values = { "#k": v }
+        values or= {}
+        
+        options = @model.getOptions {-keepDefaults, +keepUnchanged}
+        for k of @FILTER_CHART_OPTIONS
+            # console.log "filter #k?", not options[k]
+            if k in options and not options[k]
+                delete options[k]
+        options
+    
+    
+    toTemplateLocals: ->
+        attrs = _.clone @model.attributes
+        delete attrs.options
+        { @model, view:this, @graph_id, slug:'', name:'', desc:'' } import attrs
+    
+    
+    /**
+     * Resizes chart according to the model's width and height.
+     * @return { width, height }
+     */
+    resizeViewport: ->
+        modelW = width  = @model.get 'width'
+        modelH = height = @model.get 'height'
+        return { width, height } unless @ready
+        
+        # Remove old style, as it confuses dygraph after options update
+        viewport = @$ '.viewport'
+        viewport.attr 'style', ''
+        label = @$ '.graph-legend'
+        
+        if width is 'auto'
+            vpWidth = viewport.innerWidth() or 300
+            labelW = label.outerWidth() or 228
+            width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW)
+        width ?= modelW
+        if height is 'auto'
+            height = viewport.innerHeight() or 320
+        height ?= modelH
+        
+        size = { width, height }
+        viewport.css size
+        # console.log 'resizeViewport!', JSON.stringify(size), viewport
+        # @chart.resize size if forceRedraw
+        size
+    
+    
+    /**
+     * Redraw chart inside viewport.
+     */
+    renderChart: ->
+        # data = @model.get 'dataset'
+        # data = data.getData() if typeof data is not 'string'
+        dataset = @model.dataset
+        data = dataset.getData()
+        size = @resizeViewport()
+        
+        # XXX: use @model.changedAttributes() to calculate what to update?
+        options = @chartOptions() #import size
+        options import do
+            colors             : dataset.getColors()
+            labels             : dataset.getLabels()
+            labelsDiv          : @$ '.graph-legend' .0
+            valueFormatter     : @numberFormatterHTML
+            axes:
+                x:
+                    axisLabelFormatter : @axisDateFormatter
+                    valueFormatter     : @dateFormatter
+                y:
+                    axisLabelFormatter : @axisFormatter @numberFormatter
+                    valueFormatter     : @numberFormatterHTML
+        
+        # console.log "#this.render!", dataset
+        # _.dump options, 'options'
+        
+        # Always rerender the chart to sidestep the case where we need to push defaults into
+        # dygraphs to reset the current option state.
+        @chart?.destroy()
+        @chart = new Dygraph do
+            @$ '.viewport' .0
+            data
+            options
+        
+        # unless @chart
+        #     @chart = new Dygraph do
+        #         @$ '.viewport' .0
+        #         data
+        #         options
+        # else
+        #     @chart.updateOptions options
+        #     @chart.resize size
+        
+        this
+    
+    
+    /**
+     * Render the chart and other Graph-derived view components.
+     */
+    render: ->
+        return this unless @ready
+        @wait()
+        @checkWaiting()
+        GraphView.__super__.render ...
+        @renderChart()
+        @unwait()
+        @checkWaiting()
+        this
+    
+    
+    ### }}}
+    ### Formatters {{{
+    
+    # XXX: Dygraphs-specific
+    axisFormatter: (fmttr) ->
+        (n, granularity, opts, g) -> fmttr n, opts, g
+    
+    # XXX: Dygraphs-specific
+    axisDateFormatter: (n, granularity, opts, g) ->
+        moment(n).format 'MM/YYYY'
+    
+    # XXX: Dygraphs-specific
+    dateFormatter: (n, opts, g) ->
+        moment(n).format 'DD MMM YYYY'
+    
+    _numberFormatter: (n, digits=2) ->
+        for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]]
+            if n >= d
+                n = n / d
+                break
+        s = n.toFixed(digits)
+        parts = s.split '.'
+        whole = _.rchop parts[0], 3 .join ','
+        fraction = '.' + parts.slice(1).join '.'
+        { n, digits, whole, fraction, suffix }
+    
+    # XXX: Dygraphs-specific
+    numberFormatter: (n, opts, g) ->
+        digits = opts('digitsAfterDecimal') ? 2
+        { whole, fraction, suffix } = @_numberFormatter n, digits
+        "#whole#fraction#suffix"
+    
+    # XXX: Dygraphs-specific
+    numberFormatterHTML: (n, opts, g) ->
+        digits = opts('digitsAfterDecimal') ? 2
+        { whole, fraction, suffix } = @_numberFormatter n, digits
+        # coco will trim the whitespace
+        "<span class='value'>
+        <span class='whole'>#whole</span>
+        <span class='fraction'>#fraction</span>
+        <span class='suffix'>#suffix</span>
+        </span>"
+    
+    ### }}}
+    ### Event Handlers {{{
+    
+    onSync: ->
+        return unless @ready
+        console.info "#this.sync() --> success!"
+        # TODO: UI alert
+        @chartOptions @model.getOptions(), {+silent}
+        @render()
+    
+    onStartWaiting: ->
+        status = @checkWaiting()
+        # console.log "#this.onStartWaiting!", status
+    
+    onStopWaiting: ->
+        status = @checkWaiting()
+        # console.log "#this.onStopWaiting!", status
+    
+    onModelError: ->
+        console.error "#this.error!", arguments
+        # TODO: UI alert
+    
+    onModelChange: ->
+        changes = @model.changedAttributes()
+        options = @model.getOptions()
+        # 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
+    
+    # Needed because (sigh) _.debounce returns undefined, and we need to preventDefault()
+    stopAndRender : -> @render ... ; false
+    
+    
+    ### }}}
+    ### Spinner {{{
+    
+    /**
+     * Retrieve or construct the spinner.
+     */
+    spinner: ->
+        el = @$ '.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
+    
+    # }}}
+    
+
index 8ea477e..18203dd 100644 (file)
@@ -1,6 +1,7 @@
 models         = require 'kraken/graph/graph-model'
+base_views     = require 'kraken/graph/graph-view'
 display_views  = require 'kraken/graph/graph-display-view'
 edit_views     = require 'kraken/graph/graph-edit-view'
 index_views    = require 'kraken/graph/graph-list-view'
 
-exports import models import display_views import edit_views import index_views
+exports import models import base_views import display_views import edit_views import index_views
index 83179c1..6c39824 100644 (file)
@@ -6,7 +6,7 @@ section.graph-edit.graph(id=graph_id)
     
     .graph-viewport-row.row-fluid
         .viewport
-        .graph-label
+        .graph-legend
     
     .graph-settings-row.row-fluid
         .graph-settings.tabbable
index 64df61d..7a9ee67 100644 (file)
@@ -26,7 +26,7 @@ section.graph
         min-height 320px
         overflow hidden
     
-    .graph-label
+    .graph-legend
         position absolute
         z-index 100
         top 1em
@@ -51,7 +51,7 @@ section.graph
         .suffix
             text-align left
     
-    .viewport:hover + .graph-label
+    .viewport:hover + .graph-legend
         border 1px solid $light
     
     /* }}} */
index c76da7b..9e51454 100644 (file)
@@ -111,6 +111,7 @@ dev:
                 - index
             - graph:
                 - graph-model
+                - graph-view
                 - graph-display-view
                 - graph-edit-view
                 - graph-list-view