Adds AppView to start cleaning up -main.co files; Begins refactor of chart-type code...
authorDavid Schoonover <dsc@wikimedia.org>
Sun, 3 Jun 2012 12:53:20 +0000 (14:53 +0200)
committerDavid Schoonover <dsc@wikimedia.org>
Sun, 3 Jun 2012 12:53:20 +0000 (14:53 +0200)
21 files changed:
lib/app.co [new file with mode: 0644]
lib/base/asset-manager.co [new file with mode: 0644]
lib/base/base-model.co
lib/chart/chart-type.co
lib/chart/index.co
lib/chart/option/chart-option-model.co [moved from lib/chart/chart-option-model.co with 100% similarity]
lib/chart/option/chart-option-view.co [moved from lib/chart/chart-option-view.co with 99% similarity]
lib/chart/option/index.co [new file with mode: 0644]
lib/chart/type/d3-line.co [deleted file]
lib/chart/type/d3/d3-bar-chart-type.co [moved from lib/chart/type/d3-bar.co with 100% similarity]
lib/chart/type/d3/d3-geo-chart-type.co [new file with mode: 0644]
lib/chart/type/d3/d3-line-chart-type.co [moved from lib/chart/type/d3-geo.co with 100% similarity]
lib/chart/type/d3/index.co [new file with mode: 0644]
lib/chart/type/dygraphs.co [moved from lib/chart/dygraphs.co with 98% similarity]
lib/graph/graph-model.co
lib/main-edit.co
lib/server/server.co
package.co
www/css/geo-display.styl
www/modules.yaml
www/schema/d3/d3-geo-world.yaml [new file with mode: 0644]

diff --git a/lib/app.co b/lib/app.co
new file mode 100644 (file)
index 0000000..58e16ad
--- /dev/null
@@ -0,0 +1,52 @@
+Backbone = require 'backbone'
+
+{ _, op,
+} = require 'kraken/util'
+
+
+/**
+ * @class Application view, automatically attaching to an existing element
+ *  found at `appSelector`.
+ * @extends Backbone.View
+ */
+AppView = exports.AppView = Backbone.View.extend do # {{{
+    appSelector : '#content .inner'
+    
+    
+    /**
+     * @constructor
+     */
+    constructor: function AppView (options={})
+        if typeof options is 'function'
+            @initialize = options
+            options = {}
+        else
+            @initialize = that if options.initialize
+        
+        @appSelector = that if options.appSelector
+        options.el or= jQuery @appSelector .0
+        console.log "new #this", options
+        Backbone.View.call this, options
+        
+        jQuery ~> @render()
+        this
+    
+    /**
+     * Override to set up your app. This method may be passed
+     * as an option to the constructor.
+     */
+    initialize: -> # stub
+    
+    /**
+     * Append subviews.
+     */
+    render : ->
+        @$el.append @view.el if @view and not @view.$el.parent()?.length
+    
+    getClassName: ->
+        "#{@..name or @..displayName}"
+    
+    toString: ->
+        "#{@getClassName()}()"
+# }}}
+
diff --git a/lib/base/asset-manager.co b/lib/base/asset-manager.co
new file mode 100644 (file)
index 0000000..fa91012
--- /dev/null
@@ -0,0 +1,43 @@
+{ _, op,
+} = require 'kraken/util'
+{ ReadyEmitter,
+} = require 'kraken/util/event'
+
+
+
+
+class AssetManager extends ReadyEmitter
+    # Map from key/url to data.
+    assets : null
+    
+    
+    /**
+     * @constructor
+     */
+    ->
+        super ...
+        @assets = {}
+    
+    
+    
+    
+    /**
+     * Load the corresponding chart specification, which includes
+     * info about valid options, along with their types and defaults.
+     */
+    load: ->
+        return this if @ready
+        proto = @constructor::
+        jQuery.ajax do
+            url     : @SPEC_URL
+            success : (spec) ~>
+                proto.spec = 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
+    
+
+
index 5048a1d..260c398 100644 (file)
@@ -87,14 +87,24 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
      * @param {Object} [opts={}] Options:
      * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context.
      * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load.
-     * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed.
-     * @param {Boolean} [opts.force=false] If true, move forward with the load even if we're ready.
-     * @returns {this} 
+     * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed successfully.
+     * @param {String} [opts.errorEvent='load-error'] Event which signals loading has completed but failed.
+     * @param {Boolean} [opts.force=false] If true, reset ready state if we're ready before proceeding.
+     * @param {Boolean} [opts.readyIfError=false] If true, move fire the ready event when loading completes, even if it failed.
+     * @returns {this}
      */
     loader: (opts={}) ->
-        opts = { -force, startEvent:'load', completeEvent:'load-success', ...opts }
+        opts = {
+            -force
+            -readyIfError
+            startEvent    : 'load'
+            completeEvent : 'load-success'
+            errorEvent    : 'load-error'
+            ...opts
+        }
         @resetReady() if opts.force
-        return this if not opts.start or @loading or @ready
+        throw new Error('You must specify a `start` function to start loading!') unless opts.start
+        return this if @loading or @ready
         
         @wait()
         @loading = true
@@ -102,11 +112,17 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
         
         # Register a handler for the post-load event that will run only once
         @once opts.completeEvent, ~>
-            console.log "#{this}.onLoadComplete()"
+            # console.log "#{this}.onLoadComplete()"
             @loading = false
             @unwait() # terminates the `load` wait
             @trigger 'load-success', this unless opts.completeEvent is 'load-success'
             @triggerReady()
+        @once opts.errorEvent, ~>
+            # console.log "#{this}.onLoadError()"
+            @loading = false
+            @unwait() # terminates the `load` wait
+            @trigger 'load-error', this unless opts.errorEvent is 'load-error'
+            @triggerReady() if opts.readyIfError
         
         # Finally, start the loading process
         opts.start.call this
index 4da37a1..ad8fabc 100644 (file)
@@ -151,7 +151,6 @@ class exports.ChartType extends ReadyEmitter
     withView  : (@view)  -> this
     
     
-    
     /**
      * Load the corresponding chart specification, which includes
      * info about valid options, along with their types and defaults.
@@ -343,6 +342,28 @@ class exports.ChartType extends ReadyEmitter
     
     
     /**
+     * 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'
+        
+        if width is 'auto'
+            Width = viewport.innerWidth() or 300
+        width ?= modelW
+        
+        if height is 'auto'
+            height = viewport.innerHeight() or 320
+        height ?= modelH
+        
+        { width, height }
+    
+    
+    /**
      * Transforms domain data and applies it to the chart library to
      * render or update the corresponding chart.
      * 
@@ -360,11 +381,12 @@ class exports.ChartType extends ReadyEmitter
      * Transforms the domain objects into a hash of derived values using
      * chart-type-specific keys.
      * 
-     * @abstract
+     * Default implementation returns `model.getOptions()`.
+     * 
      * @returns {Object} The derived data.
      */
     transform: ->
-        ...
+        @model.getOptions()
     
     
     /**
index 8cc50ae..5a02eb2 100644 (file)
@@ -1,5 +1,5 @@
-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 import dygraphs import models import views
+chart_type = require 'kraken/chart/chart-type'
+chart_option = require 'kraken/chart/option'
+dygraphs = require 'kraken/chart/type/dygraphs'
+
+exports import chart_type import chart_option import dygraphs
similarity index 99%
rename from lib/chart/chart-option-view.co
rename to lib/chart/option/chart-option-view.co
index 6c9e2a0..1b004e1 100644 (file)
@@ -3,7 +3,7 @@
 { BaseView,
 }  = require 'kraken/base'
 { ChartOption, ChartOptionList,
-} = require 'kraken/chart/chart-option-model'
+} = require 'kraken/chart/option/chart-option-model'
 
 DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms
 
diff --git a/lib/chart/option/index.co b/lib/chart/option/index.co
new file mode 100644 (file)
index 0000000..5cdea4d
--- /dev/null
@@ -0,0 +1,4 @@
+model = require 'kraken/chart/option/chart-option-model'
+view  = require 'kraken/chart/option/chart-option-view'
+
+exports import model import view
diff --git a/lib/chart/type/d3-line.co b/lib/chart/type/d3-line.co
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/lib/chart/type/d3/d3-geo-chart-type.co b/lib/chart/type/d3/d3-geo-chart-type.co
new file mode 100644 (file)
index 0000000..4709aa0
--- /dev/null
@@ -0,0 +1,182 @@
+ColorBrewer = require 'colorbrewer'
+
+{ _, op,
+} = require 'kraken/util'
+{ ChartType,
+} = require 'kraken/chart'
+
+class GeoWorldChartType extends ChartType
+    __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]>
+    SPEC_URL : '/schema/d3/d3-geo-world.json'
+    
+    # NOTE: ChartType.register() must come AFTER `typeName` declaration.
+    typeName : 'd3-geo-world'
+    ChartType.register this
+    
+    
+    /**
+     * 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'
+    
+    
+    
+    -> super ...
+    
+    
+    transform: ->
+        options = @model.getOptions() import @determineSize()
+        # options.colors.palette = ["black", "red"] if options.colors.palette?
+        options.colors.scaleDomain = d3.extent if options.colors.scaleDomain?
+        options
+    
+    
+    getProjection : (type) ->
+        switch type
+        case 'mercator' 'albers' 'albersUsa'
+            d3.geo[type]()
+        case 'azimuthalOrtho'
+            d3.geo.azimuthal()
+                .mode 'orthographic'
+        case 'azimuthalStereo'
+            d3.geo.azimuthal()
+                .mode 'stereographic'
+        default
+            throw new Error "Invalid map projection type '#type'!"
+    
+    
+    renderChart: (data, viewport, options, lastChart) ->
+        {width, height} = options
+        
+        fill = @fill = (data, options) ->
+            d3.scale[ options.colors.scale ]()
+                .domain options.colors.scaleDomain
+                .range options.colors.palette
+        
+        quantize = @quantize = (data, options) ->
+            (d) ->
+                if data[d.properties.name]?
+                    return fill data[d.properties.name].editors
+                else
+                    # console.log 'Country '+d.properties.name+' not in data'
+                    return fill "rgb(0,0,0)"
+        
+        projection = @projection = @getProjection(options.map.projection)
+            .scale width
+            .translate [width/2, height/2]
+        
+        path = d3.geo.path()
+            .projection projection
+        
+        move = ->
+            projection
+                .translate d3.event.translate
+                .scale d3.event.scale
+            feature.attr "d", path
+        
+        zoom = d3.behavior.zoom()
+            .translate projection.translate()
+            .scale projection.scale()
+            .scaleExtent [height,height*8]
+            .on "zoom", move
+        
+        
+        ####
+        
+        chart = d3.select viewport.0
+            .append "svg:svg"
+                .attr "width", width
+                .attr "height", height
+                .append "svg:g"
+                    .attr "transform", "translate(0,0)"
+                    .call zoom
+        
+        # path objects
+        feature := map.selectAll ".feature"
+        
+        # rectangle
+        map.append "svg:rect"
+            .attr "class", "frame"
+            .attr "width", width
+            .attr "height", height
+        
+        
+        ### infobox
+        infobox := d3.select '#infobox'
+        
+        infobox.select '#ball'
+            .append "svg:svg"
+                .attr "width", "100%"
+                .attr "height", "20px"
+                .append "svg:rect"
+                    .attr "width", "60%"
+                    .attr "height", "20px"
+                    .attr "fill", '#f40500'
+        
+        setInfoBox = (d) ->
+            name = d.properties.name
+            ae = 0
+            e5 = 0
+            e100 = 0
+            
+            if data[name]?
+                ae   = parseInt data[name].editors
+                e5   = parseInt data[name].editors5
+                e100 = parseInt data[name].editors100
+            
+            infobox.select '#country' .text name
+            infobox.select '#ae' .text ae
+            infobox.select '#e5' .text e5+" ("+(100.0*e5/ae).toPrecision(3)+"%)"
+            infobox.select '#e100' .text e100+" ("+(100.0*e100/ae).toPrecision(3)+"%)"
+            
+            xy = d3.svg.mouse this
+            infobox.style "left", xy[0]+'px'
+            infobox.style "top", xy[1]+'px'
+            infobox.style "display", "block"
+        
+        
+        worldmap = ->
+            d3.json do
+                "/data/geo/maps/world-countries.json"
+                (json) ->
+                    feature := feature
+                        .data json.features
+                        .enter().append "svg:path"
+                            .attr "class", "feature"
+                            .attr "d", path
+                            .attr "fill", quantize
+                            .attr "id", (d) -> d.properties.name
+                            .on "mouseover", setInfoBox
+                            .on "mouseout", -> infobox.style "display", "none"
+        
+        
+        
+    
+
+
+main = ->
+    jQuery.ajax do
+        url : "/data/geo/data/en_geo_editors.json"
+        dataType : 'json'
+        success : (res) ->
+            # result will be the returned JSON
+            data := res
+            
+            # delete & hide spinner
+            jQuery '.geo-spinner' .spin(false).hide()
+            
+            # load the world map
+            worldmap()
+            
+            # adding bootstrap tooltips
+            # $ '.page-header' .tooltip title:"for the header it works but is useless"
+            # $ '.feature' .tooltip title:"here it doesn't work"
+            
+            console.log 'Loaded geo coding map!'
+        error : (err) -> console.error err
+
+