Adds GraphDisplayView
authordsc <dsc@less.ly>
Tue, 10 Apr 2012 00:45:30 +0000 (17:45 -0700)
committerdsc <dsc@less.ly>
Tue, 10 Apr 2012 00:45:30 +0000 (17:45 -0700)
lib/graph/graph-display-view.co
lib/graph/graph-model.co
lib/main-display.co [copied from lib/main.co with 88% similarity]
lib/main-edit.co [moved from lib/main.co with 100% similarity]
lib/template/graph-display.jade
www/css/graph-display.styl [new file with mode: 0644]
www/graph/edit.jade
www/graph/view.jade

index cdbdfd3..11ac226 100644 (file)
-root = do -> this
+moment = require 'moment'
 
-_ = require 'kraken/util/underscore'
+{ _, op,
+} = require 'kraken/util'
 { BaseView,
 } = require 'kraken/base'
-{ ChartType, DEBOUNCE_RENDER,
-} = require 'kraken/chart'
 { Graph,
 } = require 'kraken/graph/graph-model'
 
+root = do -> this
+DEBOUNCE_RENDER = 100ms
+
 
 
+/**
+ * @class View for a graph visualization encapsulating.
+ */
 GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{
-    ctorName  : 'GraphDisplayView'
+    FILTER_CHART_OPTIONS : <[ 
+        file labels visibility colors dateWindow ticker timingName xValueParser
+        axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter
+        valueFormatter xValueFormatter yValueFormatter
+    ]>
+    __bind__  : <[
+        render stopAndRender resizeViewport
+        numberFormatter numberFormatterHTML
+        onReady onSync onModelChange 
+    ]>
+    __debounce__: <[ render ]>
+    
     tagName   : 'section'
-    className : 'graph'
+    className : 'graph graph-display'
     template  : require 'kraken/template/graph-display'
     
-    # events :
-    #     'blur .value'   : 'update'
-    #     'submit .value' : 'update'
+    # events:
+    #     'click    .redraw-button'                  : 'stopAndRender'
+    #     'click    .load-button'                    : 'load'
+    
+    data  : {}
+    ready : false
     
     
     constructor: function GraphDisplayView
         BaseView ...
     
-    initialize: ->
+    initialize : (o={}) ->
+        @data = {}
         @model or= new Graph
+        @id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid)
         BaseView::initialize ...
+        # console.log "#this.initialize!"
+        
+        for name of @__debounce__
+            @[name] = _.debounce @[name], DEBOUNCE_RENDER
+        
+        @viewport = @$el.find '.viewport'
+        
+        ### Model Events
+        @model
+            .on 'ready',            @onReady, this
+            .on 'sync',             @onSync,  this
+            .on 'destroy',          @remove,  this
+            .on 'change',           @render,  this
+            .on 'change:dataset',   @onModelChange
+            .on 'change:options',   @onModelChange
+            .on 'error', ~>
+                console.error "#this.error!", arguments
+                # TODO: UI alert
+        
+        @chartOptions @model.getOptions(), {+silent}
+        
+        ### Chart Viewport
+        @resizeViewport()
+        
+        # Resize chart on window resize
+        # Note: can't debounce the method itself, as the debounce wrapper returns undefined
+        $ root .on 'resize', _.debounce @resizeViewport, DEBOUNCE_RENDER
+    
+    
+    
+    
+    load: ->
+        console.log "#this.load!"
+        @model.fetch()
+        false
+    
+    change: ->
+        @model.change()
+        this
+    
+    chartOptions: (values, opts) ->
+        # Handle @chartOptions(k, v, opts)
+        if arguments.length > 1 and typeof values is 'string'
+            [k, v, opts] = arguments
+            values = { "#k": v }
+        
+        options = @model.getOptions {-keepDefaults, +keepUnchanged}
+        for k of @FILTER_CHART_OPTIONS
+            # console.log "filter #k?", not options[k]
+            if k in options and not options[k]
+                delete options[k]
+        options
+    
+    
+    toTemplateLocals: ->
+        attrs = _.clone @model.attributes
+        delete attrs.options
+        # delete attrs.dataset
+        attrs.data = @data
+        { $, _, op, @model, view:this } import attrs
+    
+    
+    /**
+     * Resizes chart according to the model's width and height.
+     * @return { width, height }
+     */
+    resizeViewport: ->
+        modelW = width  = @model.get 'width'
+        modelH = height = @model.get 'height'
+        return { width, height } unless @ready
+        
+        # Remove old style, as it confuses dygraph after options update
+        @viewport.attr 'style', ''
+        label = @$el.find '.graph-legend'
+        
+        if width is 'auto'
+            vpWidth = @viewport.innerWidth()
+            labelW = label.outerWidth()
+            width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW)
+        width ?= modelW
+        if height is 'auto'
+            height = @viewport.innerHeight()
+        height ?= modelH
+        
+        size = { width, height }
+        @viewport.css size
+        # console.log 'resizeViewport!', JSON.stringify(size), @viewport
+        # @chart.resize size if forceRedraw
+        size
+    
+    
+    # Redraw chart inside viewport.
+    renderChart: ->
+        data = @model.get 'dataset' #.getData()
+        size = @resizeViewport()
+        
+        # XXX: use @model.changedAttributes() to calculate what to update
+        options = @chartOptions() #import size
+        options import do
+            labelsDiv          : @$el.find '.graph-legend' .0
+            valueFormatter     : @numberFormatterHTML
+            axes:
+                x:
+                    axisLabelFormatter : @axisDateFormatter
+                    valueFormatter     : @dateFormatter
+                y:
+                    axisLabelFormatter : @axisFormatter @numberFormatter
+                    valueFormatter     : @numberFormatterHTML
+        
+        # console.log "#this.render!", dataset
+        _.dump options, 'options'
+        
+        # Always rerender the chart to sidestep the case where we need to push defaults into
+        # dygraphs to reset the current option state.
+        @chart?.destroy()
+        @chart = new Dygraph do
+            @viewport.0
+            data
+            options
+        
+        # unless @chart
+        #     @chart = new Dygraph do
+        #         @viewport.0
+        #         data
+        #         options
+        # else
+        #     @chart.updateOptions options
+        #     @chart.resize size
+        
+        this
+    
+    update: ->
+        locals = @toTemplateLocals()
+        @$el.find '.graph-name' .text(locals.name or '')
+        @$el.find '.graph-desc' .html jade.filters.markdown locals.desc or ''
+        this
+    
+    render: ->
+        return this unless @ready
+        root.title = "#{@get 'name'} | GraphKit"
+        @update()
+        _.invoke @subviews, 'render'
+        @renderChart()
+        @trigger 'render', this
+        false
     
     
-    toString: -> "#{@ctorName}(#{@model})"
+    
+    
+    ### Formatters {{{
+    
+    axisFormatter: (fmttr) ->
+        (n, granularity, opts, g) -> fmttr n, opts, g
+    
+    axisDateFormatter: (n, granularity, opts, g) ->
+        moment(n).format 'MM/YYYY'
+    
+    dateFormatter: (n, opts, g) ->
+        moment(n).format 'DD MMM YYYY'
+    
+    _numberFormatter: (n, digits=2) ->
+        for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]]
+            if n >= d
+                n = n / d
+                break
+        s = n.toFixed(digits)
+        parts = s.split '.'
+        whole = _.rchop parts[0], 3 .join ','
+        fraction = '.' + parts.slice(1).join '.'
+        { n, digits, whole, fraction, suffix }
+    
+    numberFormatter: (n, opts, g) ->
+        digits = opts('digitsAfterDecimal') ? 2
+        { whole, fraction, suffix } = @_numberFormatter n, digits
+        "#whole#fraction#suffix"
+    
+    numberFormatterHTML: (n, opts, g) ->
+        digits = opts('digitsAfterDecimal') ? 2
+        { whole, fraction, suffix } = @_numberFormatter n, digits
+        """
+        <span class="value"><span class="whole">#whole</span><span class="fraction">#fraction</span><span class="suffix">#suffix</span></span>
+        """
+    
+    ### }}}
+    ### Event Handlers {{{
+    
+    
+    onReady: ->
+        return if @ready
+        $.getJSON '/datasources/all', (@data) ~>
+            console.log "(#this via GraphDisplayView).ready!"
+            @ready = true
+            @onSync()
+    
+    onSync: ->
+        return unless @ready
+        console.info "#this.sync() --> success!"
+        # TODO: UI alert
+        # @change()
+        # @model.change()
+        @render()
+    
+    onModelChange: ->
+        changes = @model.changedAttributes()
+        options = @model.getOptions()
+        # console.log "Graph.changed( options ) ->\n\tchanges: #{JSON.stringify changes}\n\toptions: #{JSON.stringify options}" #"\n\t^opts: #{JSON.stringify _.intersection _.keys(changes), _.keys(options)}"
+        @chart?.updateOptions file:that if changes?.dataset
+    
+    
+    # Needed because (sigh) _.debounce returns undefined
+    stopAndRender: ->
+        @render ...
+        false
+    
+    
+    # }}}
 # }}}
+
index 62dec85..0e8c209 100644 (file)
@@ -59,6 +59,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
             slug    : ''
             name    : ''
             desc    : ''
+            notes   : ''
             dataset : '/data/datasources/rc/rc_comscore_region_uv.csv'
             # dataset : null
             width   : 'auto'
similarity index 88%
copy from lib/main.co
copy to lib/main-display.co
index 61d2da9..1d1781f 100644 (file)
@@ -5,13 +5,9 @@ Backbone = require 'backbone'
 } = require 'kraken/util'
 { BaseView, BaseModel, BaseList,
 } = require 'kraken/base'
-{ Field, FieldList, FieldView, Scaffold,
-} = require 'kraken/scaffold'
 { ChartType, DygraphsChartType,
-  ChartOption, ChartOptionList, TagSet,
-  ChartOptionView, ChartOptionScaffold,
 } = require 'kraken/chart'
-{ Graph, GraphList, GraphEditView,
+{ Graph, GraphList, GraphDisplayView,
 } = require 'kraken/graph'
 
 
@@ -52,7 +48,7 @@ main = ->
     
     # Instantiate model & view
     graph = root.graph = new Graph data, {+parse}
-    view  = root.view  = new GraphEditView do
+    view  = root.view  = new GraphDisplayView do
         graph_spec : root.CHART_OPTIONS_SPEC # FIXME: necessary?
         model      : graph
     
similarity index 100%
rename from lib/main.co
rename to lib/main-edit.co
index 74e403e..a593597 100644 (file)
@@ -1,59 +1,18 @@
-- var id = model.id || model.cid
+include browser-helpers
 - var graph_id = view.id
-section.graph(id=graph_id)
-    form.details.form-horizontal
+section.graph.graph-display(id=view.id)
         
-        .name-row.row-fluid.control-group
-            //- label.name.control-label(for="#{id}_name"): h3 Graph Name
-            input.span6.name(type='text', id="#{id}_name", name="name", placeholder='Graph Name', value=name)
+        .graph-name-row.page-header.row-fluid
+            h1.graph-name #{name}
         
-        .row-fluid
+        .graph-viewport-row.row-fluid
             .viewport
-            .graph-label
+            .graph-legend
         
-        .row-fluid
-            .graph-settings.tabbable
-                //- nav.navbar: div.navbar-inner: div.container
-                nav
-                    ul.nav.subnav.nav-pills
-                        li:         h3 Graph
-                        li.active:  a(href="#graph-#{graph_id}-info", data-toggle="tab") Info
-                        li:         a(href="#graph-#{graph_id}-data", data-toggle="tab") Data
-                        li: a.graph-options-tab(href="#graph-#{graph_id}-options", data-toggle="tab") Options
-                        li.graph-controls
-                            input.redraw-button.btn(type="button", value="Redraw")
-                            input.load-button.btn(type="button", value="Load")
-                            input.save-button.btn-success(type="button", value="Save")
-                
-                .tab-content
-                    .graph-info-pane.tab-pane.active(id="graph-#{graph_id}-info")
-                        .row-fluid
-                            .half.control-group
-                                .control-group
-                                    label.slug.control-label(for='slug') Slug
-                                    .controls
-                                        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.
-                                .control-group
-                                    label.width.control-label(for='width') Size
-                                    .controls
-                                        input.span1.width(type='text', id='width', name='width', value=width)
-                                        |  &times; 
-                                        input.span1.height(type='text', id='height', name='height', value=height)
-                                        p.help-block Choosing 'auto' will size the graph to the viewport bounds.
-                            .half.control-group
-                                label.desc.control-label(for='desc') Description
-                                .controls
-                                    //- textarea.span3.desc(id='desc', name='desc', placeholder='Graph description.') #{desc}
-                                    <textarea class="span3 desc" id="desc" name="desc" placeholder="Graph description.">#{desc}</textarea>
-                                    p.help-block A description of the graph.
-                    
-                    .graph-data-pane.tab-pane(id="graph-#{graph_id}-data")
-                        .row-fluid
-                            label.dataset.control-label(for='dataset') Data Set
-                            .controls
-                                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.
-                    
-                    .graph-options-pane.tab-pane(id="graph-#{graph_id}-options")
-
+        .graph-details-row.row-fluid
+            .graph-desc
+                != jade.filters.markdown(desc)
+            
+            //- 
+                .graph-notes.span6
+                    != jade.filters.markdown(notes)
diff --git a/www/css/graph-display.styl b/www/css/graph-display.styl
new file mode 100644 (file)
index 0000000..29568bd
--- /dev/null
@@ -0,0 +1,249 @@
+@import 'colors'
+@import 'nib'
+
+section.graph.graph-display
+    position relative
+    max-width 900px
+    margin 0 auto
+    
+    *
+        position relative
+    
+    /* * * *  Chart & Viewport  * * * {{{ */
+    .graph-legend
+        position absolute
+        z-index 100
+        top 1em
+        right 1em
+        width 200px
+        
+        padding 1em
+        background-color rgba(255,255,255, 0.75)
+        font 12px/1.5 "helvetica neue", helvetica, arial, sans-serif
+        border 1px solid $light
+        border-radius 5px
+        
+        b
+            display inline-block
+            width 140px
+        .whole
+            display inline-block
+            width 30px
+            text-align right
+        .fraction
+            text-align left
+        .suffix
+            text-align left
+    
+    .viewport:hover + .graph-legend
+        border 1px solid $light
+    
+    .viewport
+        position relative
+        min-width 200px
+        min-height 320px
+        margin-bottom 1.5em
+        overflow hidden
+    
+    /* }}} */
+    
+    
+    /* * * *  Subnav & Tabs  * * * {{{ */
+    .graph-settings.tabbable
+        .nav
+            margin-bottom 0
+            
+            li h3
+                line-height 14px
+                margin 2px
+                padding 8px 12px
+                border-radius 5px
+            li
+                margin-right 4px
+        
+        .tab-pane
+            padding 0.5em
+            margin-top 18px
+            
+            &.graph-data-pane
+                margin-top 0
+                padding 0
+    
+    .graph-controls
+        z-index 100
+        margin-top 2px
+        
+        & > .btn, & > .btn-group
+            margin 0 0.5em
+        
+        .btn-group
+            display inline-block
+        
+        input[type="button"]
+            min-width 5em
+            text-align center
+    
+    /* }}} */
+    
+    
+    /* * * *  Graph Details  * * * {{{ */
+    form.details
+        position relative
+        
+        .name-row
+            font-size 120%
+            line-height 2em
+            input.name
+                font-size 120%
+                line-height 1.2
+                height 1.2em
+                border-color $light
+                width 98%
+        .row-fluid
+            .half.control-group
+                width 50%
+                float left
+                margin-left 0
+                margin-right 0
+            label
+                width 100px
+            .controls
+                margin-left 110px
+        .help-block
+            font-size 11px
+            line-height 1.3
+    /* }}} */
+    
+    
+    /* * * *  Chart Options  * * * {{{ */
+    .options fieldset
+        border 0px
+    
+    .field.option
+        float left
+        z-index 3
+        padding 0.5em
+        margin 0.4em
+        min-width 200px
+        max-width 250px
+        min-height 1.5em
+        line-height 1.5
+        overflow hidden
+        
+        border-radius 5px
+        background-color #ccc
+        font-size 90%
+        
+        
+        h3
+            font-size 14px
+            line-height 1.3
+            cursor pointer
+        
+        .close
+            absolute top right 0.1em
+            width 1em
+            height 1em
+            line-height 1.2em
+            text-align center
+            text-decoration none
+            z-index 10
+            cursor pointer
+            opacity 0.3
+            &:hover
+                opacity 0.6
+        
+        .shortname
+            font-weight bold
+            color white
+            min-height 1.5em
+        .name
+            display none
+            font-weight bold
+            // line-height 1.5
+            // font-size 100%
+        input.value:not([type="checkbox"])
+            width 240px
+            font-family menlo, monospace
+        .type
+            &::before
+                content "Type: "
+                font-weight bold
+        .default
+            &::before
+                content "Default: "
+                font-weight bold
+        .desc
+            position relative
+        .tags, .examples
+            cursor pointer
+        .tags
+            font-size 85%
+            &::before
+                content "Tags: "
+                font-weight bold
+            .tag
+                margin 0.2em
+                line-height 1.5
+                padding 0.2em
+                white-space nowrap
+                color white
+                background-color rgba(255,255,255, 0.15)
+                border-radius 5px
+                // border 1px solid white
+        .examples
+            display none
+            &::before
+                content "Examples"
+                font-weight bold
+            .example
+                position relative
+        
+        
+        &.collapsed
+            z-index 2
+            width auto
+            min-width 50px
+            min-height 2em
+            max-width none
+            line-height 2
+            
+            cursor pointer
+            text-align center
+            
+            *
+                display none
+            .shortname
+                display inline-block
+                min-width 50px
+                min-height auto
+        
+        
+        /* Category/Tag Colors {{{ */
+        for i in 0...length($hilites)
+            $bg_color    = $hilites[i]
+            $tag_color   = $bg_color + contrast_direction($bg_color) * 50%
+            $fg_color    = (lightness($bg_color) > 55%) ? $dark : $light
+            
+            &.category_{i}
+                color $fg_color
+                background-color $bg_color
+                // .shortname, .name, .close
+                //     color $tag_color
+                label, h1, h2, h3, .shortname, .name, .close
+                    color $fg_color
+            
+            .tag.category_{i}
+                // border 1px solid $tag_color
+                // color $tag_color
+                // background-color $bg_color
+                color $fg_color
+                background-color $bg_color
+        
+        /* }}} */
+        
+    /* }}} End Chart Options */
+
+
+
+
index 4566fe5..d51b469 100644 (file)
@@ -8,3 +8,5 @@ append styles
     mixin css('data.css')
     mixin css('isotope.css')
 
+block main-scripts
+    script(src="/js/kraken/main-edit.js?"+version)
index 699cfb3..daf3b67 100644 (file)
@@ -1 +1,10 @@
-include edit
\ No newline at end of file
+extends ../layout
+
+block title
+    title Graph | GraphKit
+
+append styles
+    mixin css('graph-display.css')
+
+block main-scripts
+    script(src="/js/kraken/main-display.js?"+version)