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
+
+
diff --git a/lib/chart/type/d3/index.co b/lib/chart/type/d3/index.co
new file mode 100644 (file)
index 0000000..3a488eb
--- /dev/null
@@ -0,0 +1,5 @@
+bar  = require 'kraken/chart/type/d3/d3-bar-chart-type'
+geo  = require 'kraken/chart/type/d3/d3-geo-chart-type'
+line = require 'kraken/chart/type/d3/d3-line-chart-type'
+
+exports import bar import geo import line
similarity index 98%
rename from lib/chart/dygraphs.co
rename to lib/chart/type/dygraphs.co
index 49c3f86..7ae8e67 100644 (file)
@@ -21,11 +21,6 @@ class exports.DygraphsChartType extends ChartType
         viewport : '.viewport'
         legend   : '.graph-legend'
     
-    
-    
-    /**
-     * @constructor
-     */
     -> super ...
     
     
index 385ad7b..c5b59ce 100644 (file)
@@ -60,19 +60,17 @@ Graph = exports.Graph = BaseModel.extend do # {{{
      * Attribute defaults.
      */
     defaults: ->
-        {
-            slug    : ''
-            name    : ''
-            desc    : ''
-            notes   : ''
-            # dataset : '/data/datasources/rc/rc_comscore_region_uv.csv'
-            # dataset : null
-            width   : 'auto'
-            height  : 320
-            chartType : 'dygraphs'
-            parents : <[ root ]>
-            options : {}
-        }
+        slug    : ''
+        name    : ''
+        desc    : ''
+        notes   : ''
+        # dataset : '/data/datasources/rc/rc_comscore_region_uv.csv'
+        # dataset : null
+        width   : 'auto'
+        height  : 320
+        chartType : 'dygraphs'
+        parents : <[ root ]>
+        options : {}
     
     url: ->
         "#{@urlRoot}/#{@get('slug')}.json"
index 18e21cb..0ccad34 100644 (file)
@@ -1,8 +1,12 @@
+{EventEmitter} = require 'events'
+
 Seq      = require 'seq'
 Backbone = require 'backbone'
 
 { _, op,
 } = require 'kraken/util'
+{ AppView, 
+} = require 'kraken/app'
 { BaseView, BaseModel, BaseList,
 } = require 'kraken/base'
 { ChartType,
@@ -49,12 +53,10 @@ main = ->
     
     # _.dump _.clone(data.options), 'data.options'
     
-    # Instantiate model & view
-    graph = root.graph = new Graph data, {+parse}
-    view  = root.view  = new GraphEditView do
-        model      : graph
-    
-    $ '#content .inner' .append view.el
+    # Instantiate app with model & view
+    root.app = new AppView ->
+        @model = root.graph = new Graph data, {+parse}
+        @view  = root.view  = new GraphEditView {@model}
 
 
 # Load data files
index 3dfc9ad..f7a4fc2 100755 (executable)
@@ -118,8 +118,9 @@ app.configure ->
         log_level : LOG_LEVEL
     app.use require('browserify') do
         mount   : '/vendor/browserify.js'
-        require : <[ events seq ]>
-        cache   : "#CWD/.cache/browserify/cache.json"
+        require : <[ seq events ]>
+        cache   : false
+        # cache   : "#CWD/.cache/browserify/cache.json"
     
     # Serve static files
     app.use express.static WWW
index a341841..3b76b1c 100644 (file)
@@ -34,7 +34,7 @@ devDependencies                 :
     'uglify-js'                 : '>= 1.2.6'
 
 scripts                         : test:'expresso'
-repository                      : type:'git', url:'git://git@less.ly:kraken-ui.git'
+repository                      : type:'git', url:'git://less.ly/kraken-ui.git'
 
 engine                          : node:'>=0.6.2'
 license                         : 'MIT'
index d4c20cc..b3195b3 100644 (file)
@@ -39,7 +39,7 @@ section.geo
       // max-width 900px
     
     .frame
-        stroke #000
+        stroke #333
         fill none
         pointer-events all
     
index aba31e8..8e40043 100644 (file)
@@ -103,10 +103,14 @@ dev:
                 - graph-list-view
                 - index
             - chart:
+                - option:
+                    - chart-option-model
+                    - chart-option-view
+                    - index
+                - type:
+                    - dygraphs
+                    - index
                 - chart-type
-                - dygraphs
-                - chart-option-view
-                - chart-option-model
                 - index
             - data:
                 - metric-model
@@ -122,6 +126,7 @@ dev:
                 - dashboard-model
                 - dashboard-view
                 - index
+            - app
 
 # -   suffix: .js
 #     paths:
diff --git a/www/schema/d3/d3-geo-world.yaml b/www/schema/d3/d3-geo-world.yaml
new file mode 100644 (file)
index 0000000..03325c8
--- /dev/null
@@ -0,0 +1,99 @@
+-   name: zoom.start
+    tags:
+    - interactivity
+    - zoom
+    type: Float
+    default: 1.0
+    desc: Initial zoom-level, where 1.0 (100% zoom) shows the full map in the
+        frame (the default).
+
+-   name: zoom.min
+    tags:
+    - interactivity
+    - zoom
+    type: Float
+    default: 1.0
+    desc: Limit to the amount the chart will zoom out, expressed as a multiplier
+        of the frame. By default, this is limited to show the whole map in the
+        frame.
+
+-   name: zoom.max
+    tags:
+    - interactivity
+    - zoom
+    type: Float
+    default: 8.0
+    desc: Limit to the amount the chart will zoom in, expressed as a multiplier
+        of the frame (8x by default).
+
+-   name: colors.palette
+    tags:
+    - color
+    - axes
+    - standard
+    type: Array
+    default: [black, red]
+    desc: Array of colors to which values are mapped (based on their position
+        in `colors.scaleDomain`).
+
+-   name: colors.scale
+    tags:
+    - color
+    - axes
+    - standard
+    type: enum
+    values:
+    - linear
+    - log
+    default: log
+    desc: Scale color differences in the map using a scale-transform (log-scale by 
+        default). Options include:
+        - Linear scaling
+        - Logarithmic scaling
+
+-   name: colors.scaleDomain
+    tags:
+    - color
+    - axes
+    type: Array
+    default: null
+    desc: Domain for scaling color differences. Uses the extent of the dataset
+        by default (and when `null`), meaning the smallest value will map to the first
+        color of the palette, and the largest value to the last color.
+
+-   name: colors.missing
+    tags:
+    - standard
+    - color
+    - data
+    type: String
+    default: 'rgba(0,0,0,0)'
+    desc: Features without values are replaced with this color (transparent by default).
+
+-   name: map.projection
+    tags:
+    - geo
+    - map
+    type: enum
+    values:
+    - mercator
+    - albers
+    - albersUsa
+    - azimuthalOrtho
+    - azimuthalStereo
+    default: mercator
+    desc: Projection for map-data (mercator by default). Options include:
+        - Spherical mercator projection
+        - Albers equal-area conic projection
+        - Composite Albers projection for the United States
+        - Orthographic Azimuthal projection
+        - Stereographic Azimuthal projection
+
+-   name: map.definition
+    tags:
+    - geo
+    - map
+    type: String
+    default: "/data/geo/maps/world-countries.json"
+    desc: Path or URL to the `geoJSON` map definition data.
+