Refactor of ChartType.
authordsc <dsc@wikimedia.org>
Tue, 22 May 2012 11:09:33 +0000 (04:09 -0700)
committerdsc <dsc@wikimedia.org>
Tue, 22 May 2012 11:09:33 +0000 (04:09 -0700)
12 files changed:
lib/base/base-mixin.co
lib/chart/chart-type.co
lib/chart/dygraphs.co
lib/chart/index.co
lib/graph/graph-display-view.co
lib/graph/graph-edit-view.co
lib/graph/graph-model.co
lib/graph/graph-view.co
lib/main-edit.co
lib/server/view-helpers.co
lib/util/event/ready-emitter.co
lib/util/parser.co

index 29a8581..c6764cb 100644 (file)
@@ -145,34 +145,30 @@ BaseBackboneMixin = exports.BaseBackboneMixin =
 
 /**
  * @class Base mixin class. Extend this to create a new mixin, attaching the
- *  donor methods as you would instance methods. Your constructor must delegate
- *  to its superclass, passing itself as the context.
- * 
- *  Then, to mingle your mixin with another class or object, invoke the mixin
- *  *without* the `new` operator:
+ *  donor methods as you would instance methods.
+ *  
+ *  To mingle your mixin with another class or object:
  *  
  *  class MyMixin extends Mixin
- *      -> return Mixin.call MyMixin, it
  *      foo: -> "foo!"
  *  
  *  # Mix into an object...
- *  o = MyMixin { bar:1 }
+ *  o = MyMixin.mix { bar:1 }
  *  
  *  # Mix into a Coco class...
  *  class Bar
- *      MyMixin this
+ *      MyMixin.mix this
  *      bar : 1
  *  
  */
 class exports.Mixin
     
     /**
-     * Mixes this mixin into If the target is not a class, a new object will be
-     * returned which inherits from the mixin.
-     * @constructor
+     * Mixes this mixin into the target. If `target` is not a class, a new
+     * object will be returned which inherits from the mixin.
      */
-    (target) ->
-        return unless target
+    @mix = (target) ->
+        return that unless target
         
         MixinClass = Mixin
         MixinClass = @constructor   if this instanceof Mixin
@@ -183,8 +179,17 @@ class exports.Mixin
         else
             target = _.clone(MixinClass::) import target
         
-        return target
+        (target.__mixins__ or= []).push MixinClass
+        target
     
+    /**
+     * Coco metaprogramming hook to propagate class properties and methods.
+     */
+    @extended = (SubClass) ->
+        SuperClass = this
+        for own k, v in SuperClass
+            SubClass[k] = v unless SubClass[k]
+        SubClass
 
 
 
index 0831a29..68357c2 100644 (file)
@@ -1,47 +1,16 @@
-{ EventEmitter,
-} = require 'events'
+moment = require 'moment'
+Backbone = require 'backbone'
 
 { _, op,
 } = require 'kraken/util'
+{ ReadyEmitter,
+} = require 'kraken/util/event'
 { Parsers, ParserMixin,
 } = require 'kraken/util/parser'
 
 
 
 /**
- * @class Specification for an option.
- */
-class exports.ChartTypeOption
-    SPEC_KEYS : <[ name type default desc tags examples ]>
-    
-    name     : null
-    type     : 'String'
-    default  : null
-    desc     : ''
-    tags     : null
-    examples : null
-    
-    
-    (@chartType, @spec) ->
-        throw new Error('Each ChartTypeOption requires a name!') unless @spec.name
-        
-        for k of @SPEC_KEYS
-            v = @spec[k]
-            @[k] = v if v?
-        @tags or= []
-        @parseValue = @chartType.getParser @type
-    
-    parseValue : Parsers.parseString
-    
-    isDefault: (v) ->
-        # @default is v
-        _.isEqual @default, v
-    
-    toString: -> "(#{@name}: #{@type})"
-
-
-
-/**
  * Map of known libraries by name.
  * @type Object
  */
@@ -50,88 +19,220 @@ KNOWN_CHART_TYPES = exports.KNOWN_CHART_TYPES = {}
 
 /**
  * @class Abstraction of a chart-type or charting library, encapsulating its
- *  logic and options.
+ *  logic and options. In addition, a `ChartType` also mediates the
+ *  transformation of the domain-specific data types (the model and its view)
+ *  with its specific needs.
+ *  
+ *  `ChartType`s mix in `ParserMixin`: when implementing a `ChartType`, you can
+ *  add or supplement parsers merely by subclassing and overriding the
+ *  corresponding `parseXXX` method (such as `parseArray` or `parseDate`).
+ * 
+ * @extends EventEmitter
+ * @borrows ParserMixin
  */
-class exports.ChartType extends EventEmitter
+class exports.ChartType extends ReadyEmitter
+    
+    ### Class Methods
+    
     /**
-     * Ordered ChartTypeOption objects.
-     * @type ChartTypeOption[]
+     * Register a new chart type.
      */
-    options_ordered : null
+    @register = (Subclass) ->
+        KNOWN_CHART_TYPES[ Subclass::typeName ] = Subclass
+    
+    /**
+     * Look up a `ChartType` by `typeName`.
+     */
+    @lookup = (name) ->
+        name = name.get('chartType') if name instanceof Backbone.Model
+        KNOWN_CHART_TYPES[name]
+    
+    /**
+     * Look up a chart type by name, returning a new instance
+     * with the given model (and, optionally, view).
+     * @returns {ChartType}
+     */
+    @create = (model, view) ->
+        return null unless Type = @lookup model
+        new Type model, view
+    
+    
+    ### Class Properties
+    /*
+     * These are "class properties": each is set on the prototype at the class-level,
+     * and the reference is therefore shared by all instances. It is expected you
+     * will not modify this on the instance-level.
+     */
+    
+    /**
+     * URL for the Chart Spec JSON. Loaded once, the first time an instance of
+     * that class is created.
+     * @type String
+     * @readonly
+     */
+    CHART_SPEC_URL : null
+    
+    /**
+     * Chart-type name.
+     * @type String
+     * @readonly
+     */
+    typeName: null
     
     /**
-     * Map of option name to ChartTypeOption objects.
-     * @type { name:ChartTypeOption, ... }
+     * Map of option name to ChartOption objects.
+     * @type { name:ChartOption, ... }
+     * @readonly
      */
     options : null
     
+    /**
+     * Ordered ChartOption objects.
+     * 
+     * This is a "class-property": it is set on the prototype at the class-level,
+     * and the reference is shared by all instances. It is expected you will not
+     * modify that instance.
+     * 
+     * @type ChartOption[]
+     * @readonly
+     */
+    options_ordered : null
+    
+    /**
+     * Hash of role-names to the selector which, when applied to the view,
+     * returns the correct element.
+     * @type Object
+     */
+    roles :
+        viewport : '.viewport'
+    
+    /**
+     * Whether the ChartType has loaded all its data and is ready.
+     * @type Boolean
+     */
+    ready: false
+    
+    
+    
+    ### Instance properties
+    
+    /**
+     * Model to be rendered as a chart.
+     * @type Backbone.Model
+     */
+    model : null
+    
+    /**
+     * View to render the chart into.
+     * @type Backbone.View
+     */
+    view  : null
+    
+    /**
+     * Last chart rendered by this ChartType.
+     * @private
+     */
+    chart: null
+    
+    
     
     
     /**
      * @constructor
-     * @param {String} name Library name.
-     * @param {Array} options List of options objects, each specifying the
-     *  name, type, default, description (etc) of a chart option.
      */
-    (@name, options) ->
-        @options_ordered = _.map options, (opt) ~> new ChartTypeOption this, opt
-        @options = _.synthesize @options_ordered, -> [it.name, it]
-        ChartType.register this
+    (@model, @view) ->
+        @roles or= {}
+        _.bindAll this, ...@__bind__ # TODO: roll up MRO
+        @loadSpec() unless @ready
+    
+    
+    # Builder Pattern
+    withModel : (@model) -> this
+    withView  : (@view)  -> this
+    
     
     
     /**
-     * @returns {ChartTypeOption} Get an option's spec by name.
+     * Load the corresponding chart specification, which includes
+     * info about valid options, along with their types and defaults.
      */
-    get: (name, def) ->
-        @options[name] or def
+    loadSpec: ->
+        return this if @ready
+        proto = @constructor::
+        jQuery.ajax do
+            url     : @CHART_SPEC_URL
+            success : (spec) ~>
+                proto.options_ordered = spec
+                proto.options = _.synthesize spec, -> [it.name, it]
+                proto.ready = true
+                @emit 'ready', this
+            error: ~> console.error "Error loading #{@typeName} spec! #it"
+        this
+    
     
     /**
-     * @returns {Array} List of values found at the given attr on each
-     *  option spec object.
+     * @returns {ChartOption} Get an option's spec by name.
      */
-    pluck: (attr) ->
-        _.pluck @spec, attr
+    getOption: (name, def) ->
+        @options[name] or def
+    
     
     /**
      * @returns {Object} An object, mapping from option.name to the
      *  result of the supplied function.
      */
     map: (fn, context=this) ->
-        _.synthesize @spec, ~> [it.name, fn.call(context, it, it.name, this)]
+        _.synthesize @options, ~> [it.name, fn.call(context, it, it.name, this)]
     
     
     /**
-     * @returns {Boolean} Whether the supplied value is the same as
-     * the default value for the given key.
+     * @param {String} attr Attribute to look up on each options object.
+     * @returns {Object} Map from name to the value found at the given attr.
      */
-    isDefault: (name, value) ->
-        @get name .isDefault value
+    pluck: (attr) ->
+        @map -> it[attr]
     
     
-    serialize: (v, k) ->
-        # if v!?
-        #     v = ''
-        if _.isBoolean v
-            v =  Number v
-        else if _.isObject v
-            v = JSON.stringify v
-        String v
+    /**
+     * @returns {Boolean} Whether the supplied value is the same as
+     *  the default value for the given key.
+     */
+    isDefault: (name, value) ->
+        _.isEqual @getOption(name).default, value
     
     
-    ### Parsers
+    ### }}}
+    ### Parsers & Serialization {{{
     
     /**
      * When implementing a ChartType, you can add or override parsers
      * merely by subclassing.
+     * @borrows ParserMixin
      */
-    ParserMixin this
+    ParserMixin.mix this
     
+    /**
+     * @returns {Function} Parser for the given option name.
+     */
     getParserFor: (name) ->
-        @getParser @get(name).type
+        @getParser @getOption(name).type
     
+    /**
+     * Parses a single serialized option value into its proper type.
+     * 
+     * @param {String} name Option-name of the value being parsed.
+     * @param {String} value Value to parse.
+     * @returns {*} Parsed value.
+     */
     parseOption: (name, value) ->
         @getParserFor(name)(value)
     
+    /**
+     * Parses options using `parseOption(name, value)`.
+     * 
+     * @param {Object} options Options to parse.
+     * @returns {Object} Parsed options.
+     */
     parseOptions: (options) ->
         out = {}
         for k, v in options
@@ -139,22 +240,174 @@ class exports.ChartType extends EventEmitter
         out
     
     
+    /**
+     * Serializes option-value to a String.
+     * 
+     * @param {*} v Value to serialize.
+     * @param {String} k Option-name of the given value.
+     * @returns {String} The serialized value
+     */
+    serialize: (v, k) ->
+        # if v!?
+        #     v = ''
+        if _.isBoolean v
+            v =  Number v
+        else if _.isObject v
+            v = JSON.stringify v
+        String v
+    
     
-    ### Class Methods
+    ### }}}
+    ### Formatters {{{
     
     /**
-     * Register a new chart type.
+     * Formats a date for display on an axis: `MM/YYYY`
+     * @param {Date} d Date to format.
+     * @returns {String}
      */
-    @register = (chartType) ->
-        KNOWN_CHART_TYPES[chartType.name] = chartType
+    axisDateFormatter: (d) ->
+        moment(d).format 'MM/YYYY'
     
     /**
-     * Look up a chart type by name.
+     * Formats a date for display in the legend: `DD MMM YYYY`
+     * @param {Date} d Date to format.
+     * @returns {String}
      */
-    @lookup = (name) ->
-        KNOWN_CHART_TYPES[name]
+    dateFormatter: (d) ->
+        moment(d).format 'DD MMM YYYY'
+    
+    /**
+     * Formats a number for display, first dividing by the greatest suffix
+     *  of {B = Billions, M = Millions, K = Thousands} that results in a
+     *  absolute value greater than 0, and then rounding to `digits` using
+     *  `result.toFixed(digits)`.
+     * 
+     * @param {Number} n Number to format.
+     * @param {Number} [digits=2] Number of digits after the decimal to always display.
+     * @returns {String} Formatted number.
+     */
+    numberFormatter: (n, digits=2) ->
+        for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]]
+            break if isNaN d
+            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 }
+    
+    
+    ### }}}
+    ### Rendering {{{
+    
+    /**
+     * Finds the element in the view which plays the given role in the chart.
+     * Canonically, all charts have a "viewport" element. Other roles might
+     * include a "legend" element, or several "axis" elements.
+     * 
+     * Default implementation looks up a selector in the `roles` hash, and if
+     * found, queries the view for matching children.
+     * 
+     * @param {String} role Name of the role to look up.
+     * @returns {jQuery|null} $-wrapped DOM element.
+     */
+    getElementsForRole: (role) ->
+        return null unless @view
+        if @roles[role]
+            @view.$ that
+        else
+            null
+    
+    
+    /**
+     * Transform/extract the data for this chart from the model. Default
+     * implementation calls `model.getData()`.
+     * 
+     * @returns {*} Data object for the chart.
+     */
+    getData: ->
+        @model.getData()
+    
     
+    /**
+     * Map from option-name to default value. Note that this reference will be
+     * modified by `.render()`.
+     * 
+     * @returns {Object} Default options.
+     */
+    getDefaultOptions: ->
+        @pluck 'default'
+    
+    
+    /**
+     * Transforms domain data and applies it to the chart library to
+     * render or update the corresponding chart.
+     * 
+     * @returns {Chart}
+     */
+    render: ->
+        data       = @getData()
+        options    = @getDefaultOptions() import @transform @model, @view
+        viewport   = @getElementsForRole 'viewport'
+        @lastChart = @renderChart data, viewport, options, @chart
+    
+    
+    /**
+     * Transforms the domain objects into a hash of derived values using
+     * chart-type-specific keys.
+     * 
+     * @abstract
+     * @returns {Object} The derived data.
+     */
+    transform: ->
+        ...
+    
+    
+    /**
+     * Called to render the chart.
+     * 
+     * @abstract
+     * @returns {Chart}
+     */
+    renderChart: (data, viewport, options, lastChart) ->
+        ...
     
+    
+    ### }}}
+
 
 
 
+# 
+# /**
+#  * @class Specification for an option.
+#  */
+# class exports.ChartOption
+#     SPEC_KEYS : <[ name type default desc tags examples ]>
+#     
+#     name     : null
+#     type     : 'String'
+#     default  : null
+#     desc     : ''
+#     tags     : null
+#     examples : null
+#     
+#     
+#     (@spec) ->
+#         throw new Error('Each ChartOption requires a name!') unless @spec.name
+#         
+#         for k of @SPEC_KEYS
+#             v = @spec[k]
+#             @[k] = v if v?
+#         @tags or= []
+#     
+#     isDefault: (v) ->
+#         # @default is v
+#         _.isEqual @default, v
+#     
+#     toString: -> "(#{@name}: #{@type})"
+# 
+# 
+# 
index bdaada1..1702859 100644 (file)
 _ = require 'kraken/util/underscore'
-{ ChartType, ChartTypeOption,
+{ ChartType,
 } = require 'kraken/chart/chart-type'
 
 
 class exports.DygraphsChartType extends ChartType
-    name : 'dygraphs'
+    __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]>
+    CHART_SPEC_URL : '/schema/dygraph.json'
+    typeName : 'dygraphs'
+    ChartType.register this
     
-    (options) ->
-        super 'dygraphs', options
     
-    render: ->
-        ...
+    /**
+     * Hash of role-names to the selector which, when applied to the view,
+     * returns the correct element.
+     * @type Object
+     */
+    roles :
+        viewport : '.viewport'
+        legend   : '.graph-legend'
     
+    
+    
+    /**
+     * @constructor
+     */
+    -> super ...
+    
+    
+    
+    ### Formatters {{{
+    
+    # XXX: Dygraphs-specific
+    makeAxisFormatter: (fmttr) ->
+        (n, granularity, opts, g) -> fmttr n, opts, g
+    
+    # XXX: Dygraphs-specific
+    dygAxisDateFormatter: (n, granularity, opts, g) ->
+        moment(n).format 'MM/YYYY'
+    
+    # XXX: Dygraphs-specific
+    dygDateFormatter: (n, opts, g) ->
+        moment(n).format 'DD MMM YYYY'
+    
+    # XXX: Dygraphs-specific
+    dygNumberFormatter: (n, opts, g) ->
+        digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2
+        { whole, fraction, suffix } = @numberFormatter n, digits
+        "#whole#fraction#suffix"
+    
+    # XXX: Dygraphs-specific
+    dygNumberFormatterHTML: (n, opts, g) ->
+        digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2
+        # 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>"
+    
+    
+    ### }}}
+    ### Rendering {{{
+    
+    /**
+     * Determines chart viewport size.
+     * @return { width, height }
+     */
+    determineSize: ->
+        modelW = width  = @model.get 'width'
+        modelH = height = @model.get 'height'
+        return { width, height } unless @view.ready and width and height
+        
+        viewport = @getElementsForRole 'viewport'
+        legend   = @getElementsForRole 'legend'
+        
+        if width is 'auto'
+            # Remove old style, as it confuses dygraph after options update
+            delete viewport.prop('style').width
+            vpWidth = viewport.innerWidth() or 300
+            legendW = legend.outerWidth() or 228
+            width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW)
+        width ?= modelW
+        
+        if height is 'auto'
+            # Remove old style, as it confuses dygraph after options update
+            delete viewport.prop('style').height
+            height = viewport.innerHeight() or 320
+        height ?= modelH
+        
+        { width, height }
+    
+    
+    resizeViewport: ->
+        @getElementsForRole 'viewport' .css @determineSize()
+        this
+    
+    
+    /**
+     * Transforms the domain objects into a hash of derived values using
+     *  chart-type-specific keys.
+     * @returns {Object} The derived chart options.
+     */
+    transform: ->
+        dataset = @model.dataset
+        options = @view.chartOptions() import @determineSize()
+        options import do
+            colors             : dataset.getColors()
+            labels             : dataset.getLabels()
+            labelsDiv          : @getElementsForRole 'legend' .0
+            valueFormatter     : @dygNumberFormatterHTML
+            axes:
+                x:
+                    axisLabelFormatter : @dygAxisDateFormatter
+                    valueFormatter     : @dygDateFormatter
+                y:
+                    axisLabelFormatter : @makeAxisFormatter @dygNumberFormatter
+                    valueFormatter     : @dygNumberFormatterHTML
+    
+    
+    /**
+     * @returns {Dygraph} The Dygraph chart object.
+     */
+    renderChart: (data, viewport, options, lastChart) ->
+        @resizeViewport()
+        
+        # console.log "#this.render!"
+        # _.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.
+        lastChart?.destroy()
+        new Dygraph viewport.0, data, options
+    
+    
+    
+    ### }}}
+
+
index e25febb..8cc50ae 100644 (file)
@@ -1,5 +1,5 @@
-chart_type = require 'kraken/chart/chart-type'
+chart      = require 'kraken/chart/chart-type'
 dygraphs   = require 'kraken/chart/dygraphs'
 models     = require 'kraken/chart/chart-option-model'
 views      = require 'kraken/chart/chart-option-view'
-exports import chart_type import dygraphs import models import views
+exports import chart import dygraphs import models import views
index eb34859..3460149 100644 (file)
@@ -69,7 +69,7 @@ GraphDisplayView = exports.GraphDisplayView = GraphView.extend do # {{{
      */
     exportChart: (evt) ->
         # The following code is dygraph specific, thus should not 
-        # be implemented in this class. Rather in the Dygraphs ChartType-subclass. 
+        # be implemented in this class. Rather in the Dygraphs Chart-subclass. 
         # The same is true for the 'renderChart' method above.
         # 
         # The Dygraph.Export module is from http://cavorite.com/labs/js/dygraphs-export/
index 05bbbac..1b70b4d 100644 (file)
@@ -110,6 +110,7 @@ GraphEditView = exports.GraphEditView = GraphView.extend do # {{{
     ### }}}
     ### Rendering {{{
     
+    # TODO: refactor this to ChartType?
     chartOptions: (values, opts) ->
         # Handle @chartOptions(k, v, opts)
         if arguments.length > 1 and typeof values is 'string'
@@ -139,7 +140,6 @@ GraphEditView = exports.GraphEditView = GraphView.extend do # {{{
         @checkWaiting()
         root.title = "#{@get 'name'} | GraphKit"
         GraphEditView.__super__.render ...
-        @renderChart()
         # @updateURL()
         @unwait()
         @isRendering = false
index 2d231f5..2f96a1b 100644 (file)
@@ -92,9 +92,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         
         @constructor.register this
         @parents = new GraphList
-        
-        # TODO: Load on-demand
-        @chartType = ChartType.lookup @get 'chartType'
+        @chartType = ChartType.create this
         
         # Insert submodels in place of JSON
         @dataset = new DataSet {id:@id, ...@get 'data'}
@@ -179,6 +177,9 @@ Graph = exports.Graph = BaseModel.extend do # {{{
                 @triggerReady 'dataReady', 'data-ready'
         this
     
+    getData: ->
+        @dataset.getData()
+    
     onDataSetChange: ->
         console.log "#this.onDataSetChange!"
         @set 'data', @dataset, {+silent}
index 4ff25d3..0a857e2 100644 (file)
@@ -29,6 +29,13 @@ GraphView = exports.GraphView = BaseView.extend do # {{{
     __debounce__: <[ render ]>
     tagName   : 'section'
     
+    /**
+     * The chart type backing this graph.
+     * @type ChartType
+     */
+    chartType : null
+    
+    
     
     constructor: function GraphView
         BaseView ...
@@ -36,6 +43,7 @@ GraphView = exports.GraphView = BaseView.extend do # {{{
     initialize : (o={}) ->
         @model or= new Graph
         @id = @graph_id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid)
+        @chartType = @model.chartType.withView this
         GraphView.__super__.initialize ...
         
         for name of @__debounce__
@@ -69,6 +77,8 @@ GraphView = exports.GraphView = BaseView.extend do # {{{
         @wait()
         Seq()
             .seq_ (next) ~>
+                @chartType.once 'ready', next.ok
+            .seq_ (next) ~>
                 @model.once 'ready', next.ok .load()
             .seq_ (next) ~>
                 @model.once 'data-ready', next.ok .loadData()
@@ -124,7 +134,7 @@ GraphView = exports.GraphView = BaseView.extend do # {{{
     
     
     toTemplateLocals: ->
-        attrs = _.clone @model.attributes
+        attrs = _.extend {}, @model.attributes
         delete attrs.options
         { @model, view:this, @graph_id, slug:'', name:'', desc:'' } import attrs
     
@@ -215,7 +225,10 @@ GraphView = exports.GraphView = BaseView.extend do # {{{
         @wait()
         @checkWaiting()
         GraphView.__super__.render ...
-        @renderChart()
+        
+        # @renderChart()
+        @chart = @chartType.render()
+        
         @unwait()
         @checkWaiting()
         this
index 64b54cf..16e4151 100644 (file)
@@ -5,7 +5,7 @@ Backbone = require 'backbone'
 } = require 'kraken/util'
 { BaseView, BaseModel, BaseList,
 } = require 'kraken/base'
-{ ChartType, DygraphsChartType,
+{ ChartType,
 } = require 'kraken/chart'
 { DataSource, DataSourceList,
 } = require 'kraken/dataset'
@@ -22,7 +22,7 @@ CHART_DEFAULT_OPTIONS = {}
 main = ->
     # Set up Dygraph chart type spec
     # TODO: load this on-demand
-    dyglib = new DygraphsChartType CHART_OPTIONS_SPEC
+    # dyglib = new DygraphsChartType CHART_OPTIONS_SPEC
     
     # Bind to URL changes
     History.Adapter.bind window, 'statechange', ->
@@ -35,12 +35,12 @@ main = ->
     data = {}
     
     # If we got querystring args, apply them to the graph
-    if loc.split '?' .1
-        data = _.uncollapseObject _.fromKV that.replace('#', '%23')
-        data.parents = JSON.parse that if data.parents
-        data.options = _.synthesize do
-            data.options or {}
-            (v, k) -> [ k, dyglib.parseOption(k,v) ]
+    # if loc.split '?' .1
+    #     data = _.uncollapseObject _.fromKV that.replace('#', '%23')
+    #     data.parents = JSON.parse that if data.parents
+    #     data.options = _.synthesize do
+    #         data.options or {}
+    #         (v, k) -> [ k, dyglib.parseOption(k,v) ]
     
     # Extract id from URL
     if match = /\/graphs\/([^\/?]+)/i.exec loc
@@ -52,22 +52,22 @@ main = ->
     # Instantiate model & view
     graph = root.graph = new Graph data, {+parse}
     view  = root.view  = new GraphEditView do
-        graph_spec : root.CHART_OPTIONS_SPEC # FIXME: necessary?
         model      : graph
     
     $ '#content .inner' .append view.el
 
 
 # Load data files
-Seq([   <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>
-])
-.parEach_ (next, [key, url]) ->
-    jQuery.ajax do
-        url : url,
-        success : (res) ->
-            root[key] = res
-            next.ok()
-        error : (err) -> console.error err
-.seq ->
-    jQuery main
+# Seq([   <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>
+# ])
+# .parEach_ (next, [key, url]) ->
+#     jQuery.ajax do
+#         url : url,
+#         success : (res) ->
+#             root[key] = res
+#             next.ok()
+#         error : (err) -> console.error err
+# .seq ->
+#     jQuery main
 
+jQuery main
index dc3ad99..70a7c66 100644 (file)
@@ -32,7 +32,8 @@ NODE_ENV = exports.NODE_ENV = (process.env.NODE_ENV or 'dev').toLowerCase()
 # NODE_ENV = exports.NODE_ENV = if IS_PROD then 'prod' else if IS_TEST then 'test' else 'dev'
 
 
-sources = exports.sources = (modulesFile, node_env=NODE_ENV) ->
+SOURCES_ENV = if process.env.KRAKEN_FORCE_BUNDLES then 'prod' else NODE_ENV
+sources = exports.sources = (modulesFile, node_env=SOURCES_ENV) ->
     # node_env = 'dev' if _.startsWith node_env, 'dev'
     mods = yaml.load fs.readFileSync modulesFile, 'utf8'
     modlist = (mods.all or []).concat (mods[node_env] or [])
index d3f893e..a247026 100644 (file)
@@ -12,10 +12,11 @@ class ReadyEmitter extends EventEmitter
     /**
      * Triggers the 'ready' event if it has not yet been triggered.
      * Subsequent listeners added to this event will be auto-triggered.
+     * @param {Boolean} [force=false] Trigger the event even if already ready.
      * @returns {this}
      */
-    triggerReady: ->
-        return this if @ready
+    triggerReady: (force) ->
+        return this if @ready and not force
         @ready = true
         @emit @readyEventName, this
         this
@@ -23,10 +24,11 @@ class ReadyEmitter extends EventEmitter
     /**
      * Resets the 'ready' event to its non-triggered state, firing a
      * 'ready-reset' event.
+     * @param {Boolean} [force=false] Trigger the event even if already reset.
      * @returns {this}
      */
-    resetReady: ->
-        return this unless @ready
+    resetReady: (force) ->
+        return this unless @ready and not force
         @ready = false
         @emit "#{@readyEventName}-reset", this
         this
@@ -46,7 +48,7 @@ class ReadyEmitter extends EventEmitter
         return this if not callback
         super ...
         if @ready and -1 is not events.split(/\s+/).indexOf @readyEventName
-            callback.call context, this
+            setTimeout ~> callback.call context, this
         this
 
 
index 42c7c80..1783703 100644 (file)
@@ -62,11 +62,11 @@ class exports.ParserMixin extends Mixin
     # - Pros: mixing in `parseXXX()` methods makes it easy to
     #   override in the target class.
     # - Cons: `parse()` is a Backbone method, which bit me once
-    #   already, so conflicts aren't unlikely.
+    #   already (hence `parseValue()`), so conflicts aren't unlikely.
     # 
     # Other ideas:
     # - Parsers live at `@__parsers__`, and each instance gets its own clone
-    # - Parser lookup uses a Cascade from the object. (Why not just use prototype, tho?)
+    # -> Parser lookup uses a Cascade from the object. (Why not just use prototype, tho?)
     
     parseValue: (v, type) ->
         @getParser(type)(v)
@@ -103,7 +103,7 @@ class exports.ParserMixin extends Mixin
  * @extends BaseModel
  * @borrows ParserMixin
  */
-ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin do
+ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin.mix do
     constructor: function ParsingModel then BaseModel ...
 
 
@@ -112,7 +112,7 @@ ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin do
  * @extends BaseList
  * @borrows ParserMixin
  */
-ParsingList = exports.ParsingList = BaseList.extend ParserMixin do
+ParsingList = exports.ParsingList = BaseList.extend ParserMixin.mix do
     constructor: function ParsingList then BaseList ...
 
 
@@ -121,7 +121,7 @@ ParsingList = exports.ParsingList = BaseList.extend ParserMixin do
  * @extends BaseView
  * @borrows ParserMixin
  */
-ParsingView = exports.ParsingView = BaseView.extend ParserMixin do
+ParsingView = exports.ParsingView = BaseView.extend ParserMixin.mix do
     constructor: function ParsingView then BaseView ...