Updates todo and adds notes from various planning meetings.
authordsc <dsc@less.ly>
Wed, 29 Feb 2012 17:03:40 +0000 (09:03 -0800)
committerdsc <dsc@less.ly>
Wed, 29 Feb 2012 17:05:37 +0000 (09:05 -0800)
16 files changed:
lib/base.co
lib/graph/graph-model.co
lib/graph/graph-view.co
lib/main.co
lib/scaffold/scaffold-model.co
lib/scaffold/scaffold-view.co
lib/server/server.co
lib/template/graph.jade
lib/util/op.co
lib/vis/vis-model.co
lib/vis/vis-view.co
www/css/colors.styl
www/css/graph.styl
www/graph/test.jade
www/layout.jade
www/modules.yaml

index f3766f4..3f96421 100644 (file)
@@ -12,8 +12,17 @@ BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{
     ctorName : 'BaseModel'
     
     initialize: ->
-        _.bindAll this, ...@__bind__ if @__bind__.length
         @__super__ = @constructor.__super__
+        _.bindAll this, ...@__bind__ if @__bind__.length
+    
+    serialize: (v) ->
+        if v!?
+            v = ''
+        else if _.isBoolean v
+            v =  Number v
+        else if _.isObject v
+            v = JSON.stringify v
+        String v
     
     /**
      * Like `.toJSON()` in that it should return a plain object with no functions,
@@ -22,7 +31,10 @@ BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{
      * @returns {Object}
      */
     toKVObject: ->
-        _.collapseObject @toJSON()
+        kvo = _.collapseObject @toJSON()
+        for k, v in kvo
+            kvo[k] = @serialize v
+        kvo
     
     /**
      * Serialize the model into a `www-form-encoded` string suitable for use as
@@ -85,6 +97,7 @@ BaseView = exports.BaseView = Backbone.View.extend do # {{{
         _.bindAll this, ...@__bind__ if @__bind__.length
         @__super__ = @constructor.__super__
         
+        @build()
         @model.view = this
         @$el.data { @model, view:this }
         @model.on 'change',  @render, this
index 2728efa..372fb66 100644 (file)
@@ -4,6 +4,8 @@ _ = require 'kraken/underscore'
 { Field, FieldList, FieldView, Scaffold,
 } = require 'kraken/scaffold'
 
+IGNORED_TAGS = exports.IGNORED_TAGS = <[ callback deprecated debugging ]>
+
 
 
 class exports.TagSet extends Array
@@ -48,6 +50,12 @@ GraphOption = exports.GraphOption = Field.extend do # {{{
         # Notify Tag indexer of category when created, to ensure all category-tags
         # get indices with colors :P
         KNOWN_TAGS.update @getCategory()
+        
+        # Ignore functions/callbacks and, ahem, hidden tags.
+        type = @get 'type', '' .toLowerCase()
+        tags = @get 'tags', []
+        if _.str.include(type, 'function') or _.intersection tags, IGNORED_TAGS .length
+            @set 'ignore', true
     
     
     # Wrapper to ensure @set('tags') is called, as tags.push()
index 4087cf5..3d87cb0 100644 (file)
@@ -58,8 +58,10 @@ GraphOptionsScaffold = exports.GraphOptionsScaffold = Scaffold.extend do # {{{
     ctorName       : 'GraphOptionsScaffold'
     tagName        : 'form'
     className      : 'options scaffold'
+    template       : require 'kraken/template/graph-scaffold'
     collectionType : GraphOptionList
     subviewType    : GraphOptionView
+    $fields        : '.fields'
     
     # GraphView will set this
     ready          : false
@@ -74,7 +76,7 @@ GraphOptionsScaffold = exports.GraphOptionsScaffold = Scaffold.extend do # {{{
         # console.log "#this.render() -> .isotope()"
         @__super__.render ...
         return this unless @ready
-        @$el.isotope do
+        @$el.find '.options.control-group' .isotope do
             itemSelector    : '.field.option'
             layoutMode      : 'masonry'
             masonry         : columnWidth : 10
index 43962e4..a90bd2a 100644 (file)
@@ -1,3 +1,4 @@
+Seq = require 'seq'
 { _, op,
 } = require 'kraken/util'
 { BaseView, BaseModel, BaseList,
 { VisView, VisModel,
 } = require 'kraken/vis'
 
-root = do -> this
+root = this
+CHART_OPTIONS_SPEC = []
+ROOT_VIS_DATA      = {}
+ROOT_VIS_OPTIONS   = {}
+
 
 # Create the Graph Scaffold
 main = ->
+    # TODO: create a preset manager
+    # Remove chart options from data so we don't have to deepcopy
+    ROOT_VIS_OPTIONS := delete root.ROOT_VIS_DATA.options
+    
+    # Bind to URL changes
     History.Adapter.bind window, 'statechange', ->
-        console.log 'StateChange!', String root.location
+        console.log 'StateChange!', String(root.location)
     
-    graph = root.graph = new VisView do
-        graph_spec : CHART_OPTIONS_SPEC
     
     # If we got querystring args, apply them to the graph
-    if root.location?.search.slice 1
-        g = _.uncollapseObject _.fromKVPairs that
-        # yarr, have to do options separately or everything goes to shit
-        options = delete g.options
-        graph.model.set g, {+silent}
-        graph.chartOptions options, {+silent}
+    data = {}
+    if String(root.location).split '?' .1
+        data = _.uncollapseObject _.fromKVPairs that.replace('#', '%23')
         
-        # Flush all changes once the DOM settles
-        _.defer do
-            -> graph.scaffold.invoke 'change'
-            50
+        # # yarr, have to do options separately or everything goes to shit
+        # options = delete data.options
+        # graph.model.set data, {+silent}
+        # graph.chartOptions options, {+silent}
+        # # Flush all changes once the DOM settles
+        # _.delay do
+        #     ->
+        #         graph.change()
+        #         # graph.scaffold.invoke 'change'
+        #     50
+    
+    
+    vis   = root.vis   = new VisModel data
+    graph = root.graph = new VisView do
+        graph_spec : root.CHART_OPTIONS_SPEC
+        model      : vis
     
     $ '#content .inner' .append graph.el
 
 
-jQuery.ajax do
-    url      : '/graph/dygraph-options.json',
-    dataType : 'json'
-    success : (data) ->
-        root.CHART_OPTIONS_SPEC = data
-        jQuery main
-    error : (err) -> console.error err
+
+# Load data files
+Seq([   <[ CHART_OPTIONS_SPEC /graph/dygraph-options.json ]>,
+        <[ ROOT_VIS_DATA /graph/root ]>
+])
+.parEach_ (next, [key, url]) ->
+    jQuery.ajax do
+        url : url,
+        dataType : 'json'
+        success : (data) ->
+            root[key] = data
+            next.ok()
+        error : (err) -> console.error err
+.seq ->
+    console.log 'All data loaded!'
+    jQuery main
 
index 8142295..65814ff 100644 (file)
@@ -14,6 +14,7 @@ Field = exports.Field = BaseModel.extend do # {{{
     
     
     initialize: ->
+        _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse'))
         @set 'value', @get('default'), {+silent} if not @has 'value'
         # console.log "#this.initialize!"
         # @on 'all', (evt) ~> console.log "#this.trigger(#evt)"
@@ -38,7 +39,6 @@ Field = exports.Field = BaseModel.extend do # {{{
         type = _ (type or @get 'type').toLowerCase()
         for t of <[ Integer Float Boolean Object Array Function ]>
             if type.startsWith t.toLowerCase()
-                # console.log "parse#t ->", @["parse#t"] unless @["parse#t"]
                 return @["parse#t"]
         @parseString
     
@@ -64,7 +64,7 @@ Field = exports.Field = BaseModel.extend do # {{{
     
     parseFunction: (fn) ->
         if fn and _.startswith String(fn), 'function'
-            try eval "(#fn)" catch err
+            try eval "(#fn)" catch err then null
         else
             null
     
@@ -90,21 +90,14 @@ Field = exports.Field = BaseModel.extend do # {{{
     
     /* * * Serializers * * */
     serializeValue: ->
-        v = @getValue()
-        if v!?
-            v = ''
-        else if _.isBoolean v
-            v =  Number v
-        else if _.isArray(v) or _.isObject(v)
-            v = JSON.stringify v
-        String v
+        @serialize @getValue()
     
     toJSON: ->
         {id:@id} import do
             _.clone(@attributes) import { value:@getValue(), def:@get('default') }
     
     toKVObject: ->
-        { "#{@id}": @serializeValue() }
+        { "#{@id}":@serializeValue() }
     
     toString: -> "(#{@id}: #{@serializeValue()})"
 # }}}
@@ -118,16 +111,17 @@ FieldList = exports.FieldList = BaseList.extend do # {{{
      * Collects a map of fields to their values, excluding those set to `null` or their default.
      * @returns {Object}
      */
-    values: (keepDefaults=false) ->
+    values: (opts={}) ->
+        opts = {-keepDefaults, -serialize} import opts
         _.synthesize do
-            if keepDefaults then @models else @models.filter -> not it.isDefault()
-            -> [ it.get('name'), it.getValue() ]
+            if opts.keepDefaults then @models else @models.filter -> not it.isDefault()
+            -> [ it.get('name'), if opts.serialize then it.serializeValue() else it.getValue() ]
     
     toJSON: ->
         @values()
     
     toKVObject: ->
-        _.collapseObject @toJSON()
+        _.collapseObject @values false, true
     
     toKVPairs: (item_delim='&', kv_delim='=') ->
         _.toKVPairs @toKVObject(), item_delim, kv_delim
index 8f2c770..5e4cbfb 100644 (file)
@@ -28,7 +28,7 @@ FieldView = exports.FieldView = BaseView.extend do # {{{
         @trigger 'update', this
     
     render: ->
-        return @remove() if @model.get 'hidden', false
+        return @remove() if @model.get 'ignore', false
         return BaseView::render ... if @template
         
         name  = @model.get 'name'
@@ -74,6 +74,7 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
     
     addOne: (field) ->
         # console.log "[S] #this.addOne!", @__super__
+        fields = if @$fields then @$el.find that else @$el
         _.remove @subviews, field.view if field.view
         
         # avoid duplicating event propagation
@@ -85,7 +86,7 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
         SubviewType = @subviewType
         view = new SubviewType model:field
         @subviews.push view
-        @$el.append view.render().el unless field.get 'hidden'
+        fields.append view.render().el unless field.get 'ignore'
         view.on 'update', @change.bind(this, field)
         
         @render()
index db5df45..3281595 100755 (executable)
@@ -2,6 +2,7 @@
 
 fs       = require 'fs'
 path     = require 'path'
+{existsSync:exists} = path
 {exec, spawn} = require 'child_process'
 
 _        = require 'underscore'
@@ -65,6 +66,9 @@ app.configure ->
     # Allow "spoofing" HTTP methods that IE doesn't support
     app.use express.methodOverride()
     
+    # Route to the web services
+    app.use app.router
+    
     # Transparently recompile modules that have changed
     app.use compiler do
         enabled : <[ coco jade-browser stylus yaml ]>
@@ -95,9 +99,6 @@ app.configure ->
     app.use express.static VAR
     app.use express.static STATIC
     
-    # Route to the web services
-    app.use app.router
-    
     # Serve directory listings
     app.use express.directory WWW
     app.use express.directory VAR
@@ -113,6 +114,13 @@ app.configure ->
 app.get '/', (req, res) ->
     res.render 'dashboard'
 
+app.get '/graph/:id', (req, res, next) ->
+    {id} = req.params
+    console.log req.url
+    if exists("#WWW/graph/#id.yaml") or exists("#WWW/graph/#id.json")
+        req.url += '.json'
+    next()
+
 
 YAML_EXT_PAT = /\.ya?ml$/i
 
@@ -167,6 +175,7 @@ app.get '/data/all', (req, res, next) ->
             console.error that if err.stack
             res.send { error:String(err), partial_data:data }
 
+
 app.get '/:type/:action', (req, res, next) ->
     {type, action} = req.params
     if path.existsSync "#WWW/#type/#action.jade"
@@ -174,6 +183,10 @@ app.get '/:type/:action', (req, res, next) ->
     else
         next()
 
+
+
+
+
 /**
  * Handle webhook notification to pull from origin.
  */
index 962f9e7..0216850 100644 (file)
@@ -1,37 +1,35 @@
 - var id = model.id || model.cid, graph_id = _.domize('graph', id)
 section.graph(id=graph_id)
-    .graph-label
-    .viewport
-    
     form.details.form-horizontal
         
         .name-row.row-fluid.control-group
             //- label.name.control-label(for="#{id}_name"): h3 Graph Name
-            .controls: input.span6.name(type='text', id="#{id}_name", placeholder='Graph Name', value=name)
+            input.span6.name(type='text', id="#{id}_name", name="name", placeholder='Graph Name', value=name)
+        
+        .viewport
+        .graph-label
         
         .row-fluid
             .half.control-group
                 label.slug.control-label(for='slug') Slug
                 .controls
-                    input.span3.slug(type='text', id='slug', placeholder='graph_slug', value=slug)
+                    input.span3.slug(type='text', id='slug', name='slug', placeholder='graph_slug', value=slug)
                     p.help-block The slug uniquely identifies this graph and will be displayed in the URL.
             .half.control-group
                 label.dataset.control-label(for='dataset') Data Set
                 .controls
-                    input.span3.dataset(type='text', id='dataset', placeholder='URL to dataset file', value=dataset)
+                    input.span3.dataset(type='text', id='dataset', name='dataset', placeholder='URL to dataset file', value=dataset)
                     p.help-block This dataset filename will soon be replaced by a friendly UI.
         
         .row-fluid
             .half.control-group
                 label.width.control-label(for='width') Width
                 .controls
-                    input.span1.width(type='text', id='width', value=width)
+                    input.span1.width(type='text', id='width', name='width', value=width)
                     p.help-block Choosing 'auto' will size the graph to the viewport bounds.
             .half.control-group
                 label.height.control-label(for='height') Height
-                .controls: input.span1.height(type='text', id='height', value=height)
+                .controls: input.span1.height(type='text', id='height', name='height', value=height)
         
     
-    fieldset.options
-        legend Graph Options
-        
+
index a47465b..ad42271 100644 (file)
@@ -57,7 +57,7 @@ module.exports = op =
     toInt       : (v) -> parseInt v
     toFloat     : (v) -> parseFloat v
     toStr       : (v) -> String v
-    toObject    : (v) -> if typeof v is 'string' then JSON.parse v else v
+    toObject    : (v) -> if typeof v is 'string' then JSON.parse(v) else v
     
     # comparison
     cmp       : (x,y)  ->  if x < y then -1 else (if x > y then 1 else 0)
index 5bd89c6..99cd073 100644 (file)
@@ -2,6 +2,7 @@ _ = require 'kraken/underscore'
 { BaseModel, BaseView,
 } = require 'kraken/base'
 
+root = do -> this
 
 
 /**
@@ -10,26 +11,44 @@ _ = require 'kraken/underscore'
  */
 VisModel = exports.VisModel = BaseModel.extend do # {{{
     ctorName    : 'VisModel'
-    urlRoot     : '/graphs'
+    urlRoot     : '/graph'
     idAttribute : 'slug'
     
     
     initialize : ->
+        BaseModel::initialize ...
         name = @get 'name'
-        if name and not (@id or @get('slug'))
+        if name and not @get 'slug', @id
             @set 'slug', _.underscored name
     
+    
     defaults: ->
         {
             slug    : ''
             name    : ''
-            dataset : '/data/pageviews_by.timestamp.language.csv'
             desc    : ''
+            dataset : '/data/pageviews_by.timestamp.language.csv'
+            # presets : []
             width   : 'auto'
             height  : 320
-            options : {}
-        }
+            options : {} import root.ROOT_VIS_OPTIONS
+        } import root.ROOT_VIS_DATA
+    
     
+    parse: (data) ->
+        data = JSON.parse data if typeof data is 'string'
+        for k, v in data
+            data[k] = Number v if _.contains(<[ width height ]>, k) and v is not 'auto'
+        data
+    
+    set: (values, opts) ->
+        if arguments.length > 1 and typeof values is 'string'
+            [k, v, opts] = arguments
+            values = { "#k": v }
+        BaseModel::set.call this, @parse(values), opts
+    
+    
+    ### Chart Option Accessors ###
     
     hasOption: (key) ->
         options = @get 'options', {}
@@ -51,6 +70,7 @@ VisModel = exports.VisModel = BaseModel.extend do # {{{
         @trigger "change:options:#key", this, value, key, opts unless opts.silent
     
     
+    
     toString: -> "#{@ctorName}(id=#{@id}, name=#{@get 'name'}, dataset=#{@get 'dataset'})"
 
 # }}}
index b707d58..71ff152 100644 (file)
@@ -26,8 +26,10 @@ VisView = exports.VisView = BaseView.extend do # {{{
     template  : require 'kraken/template/graph'
     
     events:
-        'keypress form.options .value' : 'onKeypress'
-        'submit   form.options'        : 'onSubmit'
+        'keypress form.details input[type="text"]' : 'onKeypress'
+        'keypress form.options .value'             : 'onKeypress'
+        'submit   form.details'                    : 'onDetailsSubmit'
+        'submit   form.options'                    : 'onOptionsSubmit'
     
     ready: false
     
@@ -51,11 +53,10 @@ VisView = exports.VisView = BaseView.extend do # {{{
             # console.log 'Model.changed(options) ->', changes
             @chartOptions changes, {+silent}
         
-        @build()
         @viewport = @$el.find '.viewport'
         
         @scaffold = new GraphOptionsScaffold
-        @$el.find 'fieldset' .append @scaffold.el
+        @$el.append @scaffold.el
         @scaffold.collection.reset that if o.graph_spec
         
         @scaffold.on 'change', (scaffold, value, key, field) ~>
@@ -65,13 +66,21 @@ VisView = exports.VisView = BaseView.extend do # {{{
         options = @model.get 'options', {}
         @chartOptions options, {+silent}
         
+        @resizeViewport()
         _.delay @onReady, DEBOUNCE_RENDER
     
     
     onReady: ->
+        console.log 'VisView.ready!'
         @ready = @scaffold.ready = true
+        @change()
         @renderAll()
     
+    change: ->
+        @model.change()
+        @scaffold.invoke 'change'
+        this
+    
     
     chartOptions: (values, opts) ->
         # Handle @chartOptions(k, v, opts)
@@ -85,7 +94,7 @@ VisView = exports.VisView = BaseView.extend do # {{{
                 options.get(k)?.setValue v, opts
             this
         else
-            options.values()
+            options.values() # TODO: pull this from model (must clone), sort out events
     
     
     /**
@@ -93,7 +102,7 @@ VisView = exports.VisView = BaseView.extend do # {{{
      * @return { width, height }
      */
     resizeViewport: ->
-        return this unless @ready and @chart
+        return this unless @ready
         
         # Remove old style, as it confuses dygraph after options update
         @viewport.attr 'style', ''
@@ -104,23 +113,19 @@ VisView = exports.VisView = BaseView.extend do # {{{
             height = @viewport.height()
         size = { width, height }
         @viewport.css size
-        console.log 'resizeViewport!', JSON.stringify(size), @viewport
+        # console.log 'resizeViewport!', JSON.stringify(size), @viewport
         # @chart.resize size if forceRedraw
         size
     
+    
     render: ->
         return this unless @ready
-        
-        # console.log "#this"
-        # console.log do 
-        #     "  .viewport.{ width=%s, height=%s, style=%s }"
-        #     @viewport.css('width')
-        #     @viewport.css('height')
-        #     @viewport.attr 'style'
-        # console.log '  .options:', JSON.stringify options
+        console.log 'VisView.render!'
         
         size = @resizeViewport()
-        options = @chartOptions()
+        options = @chartOptions() import size
+        options.labelsDiv = @$el.find '.graph-label' .0
+        
         # @chart?.destroy()
         unless @chart
             @chart = new Dygraph do
@@ -131,15 +136,18 @@ VisView = exports.VisView = BaseView.extend do # {{{
             @chart.updateOptions options
             @chart.resize size
         
-        path = root.location?.path or '/'
-        url = "#path?#{@toKVPairs()}"
-        # console.log 'History.pushState', url
-        History.pushState url, @model.get('name', root.document?.title or ''), url
+        # path = String(root.location?.path or '/')
+        data  = @toJSON()
+        title = @model.get('name', root.document?.title or '')
+        url   = "?"+@toKVPairs()
+        # console.log 'History.pushState', JSON.stringify(data), title, url
+        History.pushState data, title, url
         
         this
     
     renderAll: ->
         return this unless @ready
+        console.log 'VisView.renderAll!'
         _.invoke @scaffold.subviews, 'render'
         @scaffold.render()
         @render()
@@ -148,11 +156,23 @@ VisView = exports.VisView = BaseView.extend do # {{{
     onKeypress: (evt) ->
         $(evt.target).submit() if evt.keyCode is 13
     
-    onSubmit: ->
-        # console.log "#this.onSubmit!"
+    onDetailsSubmit: ->
+        console.log "#this.onDetailsSubmit!"
+        data = _.synthesize do
+            @$el.find('form.details').serializeArray()
+            -> [it.name, it.value]
+        console.log @$el, JSON.stringify data
+        @model.set data
+        false
+    
+    onOptionsSubmit: ->
+        console.log "#this.onOptionsSubmit!"
         @render()
         false
     
+    toJSON: ->
+        @model.toJSON()
+    
     toKVPairs: ->
         @model.toKVPairs.apply @model, arguments
     
index 4c836b6..a486891 100644 (file)
@@ -41,8 +41,8 @@ $hilite_favs = ($hilite_dkblue $hilite_ltblue $hilite_cyan $hilite_lime $hilite_
 
 
 /* background colors */
-$main_bgcolor   = white
-$page_bgcolor   = $light
+$main_bgcolor   = $light
+$page_bgcolor   = white
 $foot_bgcolor   = #3b3b3b
 
 /* text & link colors */
index 4f9b09f..78ce592 100644 (file)
@@ -8,7 +8,19 @@ section.graph
         position relative
     
     .graph-label
-        position relative
+        position absolute
+        z-index 100
+        top 1em
+        right 1em
+        max-width 300px
+        
+        padding 1em
+        border-radius 5px
+        background-color rgba(255,255,255, 0.75)
+        font 12px/1.3 "helvetica neue", helvetica, arial, sans-serif
+    
+    .viewport:hover + .graph-label
+        border 1px solid $light
     
     .viewport
         position relative
@@ -17,13 +29,19 @@ section.graph
         margin-bottom 1.5em
         overflow hidden
     
+    
     form.details
         position relative
         
         .name-row
+            font-size 120%
             line-height 2em
-            // input.name
-            //     width 50%
+            input.name
+                font-size 120%
+                line-height 1.2
+                height 1.2em
+                border-color $light
+                // width 50%
         .row-fluid
             .half.control-group
                 width 50%
@@ -38,11 +56,8 @@ section.graph
             font-size 11px
             line-height 1.3
     
-    fieldset.options
-        border-radius 5px
-        legend
-            width auto
-            display inline-block
+    .options fieldset
+        border 0px
     
     .field.option
         float left
index 906ecf6..5033daa 100644 (file)
@@ -8,4 +8,3 @@ append styles
     mixin css('graph.css')
     mixin css('isotope.css')
 
-
index a76e3ee..ba83c88 100644 (file)
@@ -21,16 +21,17 @@ html
     
     body
         block body
-            header.navbar.navbar-fixed-top: .navbar-inner: .container
-                block header
-                    h1: a(href="/") Kraken
-                    block nav
-                        nav#menu: ul
-                            block menu-items
-                                li.home Home
+            //- 
+                header
+                    block header
+                        h1: a(href="/") Kraken
+                        block nav
+                            nav#menu: ul
+                                block menu-items
+                                    li.home Home
             
             section#content
-                .spacer
+                //- .spacer
                 .inner
                     block content
             
index 6afc2de..e089e90 100644 (file)
@@ -39,6 +39,7 @@ all:
             - template:
                 - graph.jade
                 - graph-option.jade
+                - graph-scaffold.jade
             - scaffold:
                 - scaffold-model
                 - scaffold-view