lib
authordsc <david.schoonover@gmail.com>
Mon, 20 Feb 2012 02:14:39 +0000 (18:14 -0800)
committerdsc <david.schoonover@gmail.com>
Mon, 20 Feb 2012 02:14:39 +0000 (18:14 -0800)
Scaffold now connected to graph, and re-renders on submit.

lib/graph.co [new file with mode: 0644]
lib/main.co
lib/scaffold.co [new file with mode: 0644]
lib/server/server.co
lib/underscore/underscore.array.co
lib/util/op.co
www/css/graph.styl
www/dygraph-options.json
www/graph.jade
www/layout.jade

diff --git a/lib/graph.co b/lib/graph.co
new file mode 100644 (file)
index 0000000..97c534d
--- /dev/null
@@ -0,0 +1,96 @@
+_ = require 'kraken/underscore'
+{ Field, FieldList, FieldView, Scaffold
+} = require 'kraken/scaffold'
+
+
+GraphModel = exports.GraphModel = Backbone.Model.extend do
+    urlRoot : '/graphs'
+    
+    initialize : ->
+        name = @get 'name'
+        if name and not (@id or @has 'id')
+            @id = @attributes.id = _.underscored name
+    
+    defaults : ->
+        {
+            name    : 'Kraken Graph'
+            dataset : 'data/page_views_by_language.csv'
+            options : {}
+        }
+    
+
+
+Graph = exports.Graph = Backbone.View.extend do
+    tagName : 'section'
+    className : 'graph'
+    
+    events:
+        'keypress input'       : 'onKeypress'
+        'submit form.settings' : 'onSubmit'
+    
+    
+    initialize : (o={}) ->
+        @model or= new GraphModel
+        
+        @$el.data { model:@model, view:this }
+        @model.on 'change',  @render, this
+        @model.on 'destroy', @remove, this
+        
+        @viewport = @$el.find '.viewport'
+        @scaffold = new Scaffold do
+            el: @$el.find 'form.settings'
+        @scaffold.collection.reset CHART_OPTIONS_SPEC
+        
+        @render()
+    
+    
+    chartOptions: (values) ->
+        options = @scaffold.collection
+        
+        # @chartOptions 'label', 'label value!'
+        if arguments.length > 1
+            [k, v] = arguments
+            values = { "#k": v }
+        
+        if values
+            for k, v in values
+                options.get(k)?.setValue v
+            this
+        else
+            options.values()
+    
+    render: ->
+        @viewport.empty()
+        
+        # Remove old style, as it confuses dygraph after options update
+        @viewport.attr 'style', ''
+        console.log do 
+            'viewport.{ width=%s, height=%s, style=%s }'
+            @viewport.css('width')
+            @viewport.css('height')
+            @viewport.attr 'style'
+        console.log 'options:', JSON.stringify @chartOptions()
+        
+        @chart?.destroy()
+        @chart = new Dygraph do
+            @viewport.0
+            'data/page_views_by_language.csv'
+            @chartOptions()
+    
+    onKeypress: (evt) ->
+        $(evt.target).submit() if evt.keyCode is 13
+    
+    onSubmit: ->
+        console.log 'Graph.onSubmit!'
+        @render()
+        false
+    
+    toString: ->
+        "Graph()"
+
+
+
+
+    
+    
+
index ccb2e39..6e54704 100644 (file)
@@ -1,11 +1,22 @@
-g = null
+_       = require 'kraken/underscore'
+{ Field, FieldList, FieldView, Scaffold
+}       = require 'kraken/scaffold'
+{Graph} = require 'kraken/graph'
 
+
+
+
+root = do -> this
 main = ->
-    g := new Dygraph do
-          $ '.graph' .eq 0 .find '.viewport' .0
-          'data/page_views_by_language.csv'
-          # {'logscale': true}
+    console.log 'main()'
+    
+    # root.g = new Dygraph do
+    #       $ '.graph' .eq 0 .find '.viewport' .0
+    #       'data/page_views_by_language.csv'
+    #       # {'logscale': true}
     
+    graph = root.graph = new Graph do
+        el : $ 'section.graph' .eq 0
     
 
 jQuery main
\ No newline at end of file
diff --git a/lib/scaffold.co b/lib/scaffold.co
new file mode 100644 (file)
index 0000000..78f1f20
--- /dev/null
@@ -0,0 +1,179 @@
+_  = require 'kraken/underscore'
+op = require 'kraken/util/op'
+
+
+
+Field = exports.Field = Backbone.Model.extend do # {{{
+    idAttribute : 'name'
+    
+    initialize: ->
+        @set 'value', @get('default'), {+silent} if not @has 'value'
+    
+    defaults: ->
+        {
+            name     : ''
+            type     : 'String'
+            default  : null
+            desc     : ''
+            category : 'General'
+            tags     : []
+            examples : []
+        }
+    
+    getParser: (type) ->
+        type or= @get 'type'
+        t = _ type.toLowerCase()
+        
+        parser = op.toStr
+        if t.startsWith 'integer'
+            parser = op.toInt
+        if t.startsWith 'float'
+            parser = op.toFloat
+        if t.startsWith 'boolean'
+            parser = op.toBool
+        if t.startsWith 'object' or t.startsWith 'array'
+            parser = op.toObject
+        if t.startsWith 'function'
+            parser = (fn) -> eval "(#fn)"
+        
+        # TODO: handle 'or' by returning an array of parsers
+        parser
+    
+    
+    getValue: ->
+        @getParser() @get 'value'
+    
+    setValue: (v, options) ->
+        def = @get 'default'
+        if not v and def == null
+            val = null
+        else
+            val = @getParser()(v)
+        @set 'value', val, options
+    
+    clearValue: ->
+        @set 'value', @get('default')
+    
+    isDefault: ->
+        @get('value') is @get('default')
+    
+# }}}
+
+
+FieldList = exports.FieldList = Backbone.Collection.extend do # {{{
+    model : Field
+    
+    /**
+     * 
+     */
+    values: ->
+        _.synthesize do
+            @models.filter -> not it.isDefault()
+            -> [ it.get('name'), it.getValue() ]
+        
+        # @reduce do
+        #     (acc, field) ->
+        #         k = field.get 'name'
+        #         v = field.getValue()
+        #         if k and not field.isDefault() and v?
+        #             acc[k] = v
+        #         acc
+        #     {}
+    
+# }}}
+
+
+### Views
+
+FieldView = exports.FieldView = Backbone.View.extend do # {{{
+    tagName   : 'div'
+    className : 'field'
+    
+    events :
+      'blur .value'   : 'update'
+      'submit .value' : 'update'
+    
+    
+    
+    initialize: ->
+        @$el.data { model:@model, view:this }
+        @model.on 'change',  @render, this
+        @model.on 'destroy', @remove, this
+    
+    update: ->
+        val     = @$el.find('.value').val()
+        current = @model.get 'value'
+        return if val is current
+        
+        console.log "#this.update( #current -> #val )"
+        @model.setValue val, {+silent}
+    
+    render: ->
+        return @remove() if @model.get 'hidden'
+        
+        name  = @model.get 'name'
+        id    = _.camelize name
+        label = _.humanize name
+        @$el.html """
+            <label class="name" for="#id">#label</label>
+            <input class="value" type="text" id="#id" name="#id">
+        """
+        # @$el.find '.value' .attr 'value', @model.get 'value'
+        @$el.find '.value' .val @model.get 'value'
+        this
+    
+    remove: ->
+        @$el.remove()
+        this
+    
+    clear: ->
+        @model.destroy()
+        this
+    
+    toString: ->
+        "FieldView(#{@model.id})"
+    
+# }}}
+
+
+# There are several special options that, if passed, will be attached directly to the view:
+#   model, collection, el, id, className, tagName, attributes
+
+Scaffold = exports.Scaffold = Backbone.View.extend do # {{{
+    tagName        : 'form'
+    className      : 'scaffold'
+    
+    collectionType : FieldList
+    subviewType    : FieldView
+    
+    
+    initialize: ->
+        _.bindAll this, 'addOne', 'addAll'
+        # @subviews = []
+        
+        CollectionType = @collectionType
+        @collection or= new CollectionType
+        @collection.on 'add',   @addOne
+        @collection.on 'reset', @addAll
+        # @collection.on 'all',   @render
+        
+        @$el.addClass @className
+            .data { model:@collection, view:this }
+            .attr { action:'/save', method:'get' }
+    
+    
+    addOne: (field) ->
+        SubviewType = @subviewType
+        view = new SubviewType model:field
+        # @subviews.push view
+        @$el.append view.render().el
+        view
+    
+    addAll: ->
+        # _.invoke @subviews, 'remove'
+        # @subviews = []
+        @collection.each @addOne
+        this
+    
+# }}}
+
index b99db7c..a7dc7ab 100755 (executable)
@@ -64,7 +64,7 @@ app.configure ->
     app.use compiler do
         src     : WWW
         dest    : VAR
-        enabled : <[ stylus coco ]>
+        enabled : <[ stylus coco pyyaml ]>
         options : stylus : { nib:true, include:"#WWW/css" }
         log_level : log_level
     
index 4031dcb..0d5f1d3 100644 (file)
@@ -17,7 +17,7 @@ _array = do
         _.reduce do
             o
             (acc, [k, v], idx) ->
-                if k and filter(v)
+                if k and filter(v, k)
                     acc[k] = v
                 acc
             {}
index 83a75bc..c7d3bfc 100644 (file)
@@ -53,10 +53,11 @@ module.exports = op =
     
     # type coercion (w/ limited parameters for mapping)
     parseBool   : parseBool
-    toBool      : (v) -> !! v
+    toBool      : parseBool
     toInt       : (v) -> parseInt v
     toFloat     : (v) -> parseFloat v
     toStr       : (v) -> String v
+    toObject    : (v) -> JSON.parse v
     
     # comparison
     cmp       : (x,y)  ->  if x < y then -1 else (if x > y then 1 else 0)
index 84f5d4b..4bd91d8 100644 (file)
@@ -1,2 +1,4 @@
 @import 'nib'
 @import 'colors'
+
+
index b7d7cc1..71a3aa6 100644 (file)
@@ -1 +1 @@
-[{"category": "Overall display", "name": "width", "tags": ["overall display"], "default": 480, "examples": ["linear-regression-addseries", "annotation", "century-scale", "color-visibility", "daylight-savings", "demo", "drawing", "independent-series", "link-interaction", "linear-regression-fractions", "linear-regression", "no-range", "small-range-zero", "reverse-y-axis", "color-cycle", "multi-scale", "value-axis-formatters"], "type": "Integer", "desc": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."}, {"category": "Overall display", "name": "height", "tags": ["overall display"], "default": 320, "examples": ["annotation", "century-scale", "color-visibility", "demo", "drawing", "link-interaction", "no-range", "small-range-zero", "reverse-y-axis", "color-cycle", "multi-scale", "value-axis-formatters"], "type": "Integer", "desc": "Height, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."}, {"category": "Annotations", "name": "annotationClickHandler", "tags": ["annotations", "handler"], "default": null, "examples": ["annotation"], "type": "function(annotation, point, dygraph, event)", "desc": "If provided, this function is called whenever the user clicks on an annotation."}, {"category": "Annotations", "name": "annotationDblClickHandler", "tags": ["annotations", "handler"], "default": null, "examples": ["annotation"], "type": "function(annotation, point, dygraph, event)", "desc": "If provided, this function is called whenever the user double-clicks on an annotation."}, {"category": "Annotations", "name": "annotationMouseOutHandler", "tags": ["annotations", "handler"], "default": null, "examples": ["annotation"], "type": "function(annotation, point, dygraph, event)", "desc": "If provided, this function is called whenever the user mouses out of an annotation."}, {"category": "Annotations", "name": "annotationMouseOverHandler", "tags": ["annotations", "handler"], "default": null, "examples": ["annotation"], "type": "function(annotation, point, dygraph, event)", "desc": "If provided, this function is called whenever the user mouses over an annotation."}, {"category": "Annotations", "name": "displayAnnotations", "tags": ["annotations"], "default": false, "examples": ["annotation-gviz"], "type": "Boolean", "desc": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart."}, {"category": "Axes", "name": "avoidMinZero", "tags": ["axes"], "default": false, "examples": ["avoidMinZero"], "type": "Boolean", "desc": "When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."}, {"category": "Axes", "name": "axis", "tags": ["axes"], "default": null, "examples": ["two-axes", "steps", "two-axes-vr", "value-axis-formatters"], "type": "String or Object", "desc": "Set to either an object ({}) filled with options for this axis or to the name of an existing data series with its own axis to re-use that axis. See tests for usage."}, {"category": "Axes", "name": "axisLabelColor", "tags": ["axes"], "default": "black", "type": "String", "desc": "Color for x- and y-axis labels. This is a CSS color string."}, {"category": "Axes", "name": "axisLabelFontSize", "tags": ["axes"], "default": 14, "type": "Integer", "desc": "Size of the font (in pixels) to use in the axis labels, both x- and y-axis."}, {"category": "Axes", "name": "axisLabelFormatter", "tags": ["axes"], "default": "Depends on the data type", "examples": ["x-axis-formatter", "y-axis-formatter", "value-axis-formatters"], "type": "function(number or Date, granularity, opts, dygraph)", "desc": "Function to call to format the tick values that appear along an axis. This is usually set on a per-axis basis. The first parameter is either a number (for a numeric axis) or a Date object (for a date axis). The second argument specifies how fine-grained the axis is. For date axes, this is a reference to the time granularity enumeration, defined in dygraph-tickers.js, e.g. Dygraph.WEEKLY. opts is a function which provides access to various options on the dygraph, e.g. opts('labelsKMB')."}, {"category": "Axes", "name": "axisLabelWidth", "tags": ["axes", "labels"], "default": 50, "type": "Integer", "desc": "Width (in pixels) of the containing divs for x- and y-axis labels. For the y-axis, this also controls"}, {"category": "Axes", "name": "axisLineColor", "tags": ["axes"], "default": "black", "examples": ["demo"], "type": "String", "desc": "Color of the x- and y-axis lines. Accepts any value which the HTML canvas strokeStyle attribute understands, e.g. 'black' or 'rgb(0, 100, 255)'."}, {"category": "Axes", "name": "axisLineWidth", "tags": ["axes"], "default": 0.3, "type": "Float", "desc": "Thickness (in pixels) of the x- and y-axis lines."}, {"category": "Axes", "name": "axisTickSize", "tags": ["axes"], "default": "3.0", "type": "Number", "desc": "The size of the line to display next to each tick mark on x- or y-axes."}, {"category": "Axes", "name": "dateWindow", "tags": ["axes"], "default": "Full range of the input is shown", "examples": ["dateWindow", "drawing", "is-zoomed-ignore-programmatic-zoom", "link-interaction", "synchronize", "zoom"], "type": "Array of two Dates or numbers", "desc": "Initially zoom in on a section of the graph. Is of the form [earliest, latest], where earliest/latest are milliseconds since epoch. If the data for the x-axis is numeric, the values in dateWindow must also be numbers."}, {"category": "Axes", "name": "drawXAxis", "tags": ["axes"], "default": true, "examples": ["unboxed-spark"], "type": "Boolean", "desc": "Whether to draw the x-axis. Setting this to false also prevents x-axis ticks from being drawn and reclaims the space for the chart grid/lines."}, {"category": "Axes", "name": "drawYAxis", "tags": ["axes"], "default": true, "examples": ["drawing", "unboxed-spark"], "type": "Boolean", "desc": "Whether to draw the y-axis. Setting this to false also prevents y-axis ticks from being drawn and reclaims the space for the chart grid/lines."}, {"category": "Axes", "name": "includeZero", "tags": ["axes"], "default": false, "examples": ["no-range", "numeric-gviz", "small-range-zero"], "type": "Boolean", "desc": "Usually, dygraphs will use the range of the data plus some padding to set the range of the y-axis. If this option is set, the y-axis will always include zero, typically as the lowest value. This can be used to avoid exaggerating the variance in the data"}, {"category": "Axes", "name": "logscale", "tags": ["axes"], "default": false, "examples": ["logscale", "stock"], "type": "Boolean", "desc": "When set for a y-axis, the graph shows that axis in log scale. Any values less than or equal to zero are not displayed. Not compatible with showZero, and ignores connectSeparatedPoints. Also, showing log scale with valueRanges that are less than zero will result in an unviewable graph."}, {"category": "Axes", "name": "panEdgeFraction", "tags": ["axes", "interactive elements"], "default": null, "examples": ["zoom"], "type": "Float", "desc": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."}, {"category": "Axes", "name": "pixelsPerLabel", "tags": ["axes", "grid"], "default": "60 (x-axis) or 30 (y-axes)", "examples": ["value-axis-formatters"], "type": "Integer", "desc": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks. This is set on a per-axis basis."}, {"category": "Axes", "name": "ticker", "tags": ["axes"], "default": "Dygraph.dateTicker or Dygraph.numericTicks", "type": "function(min, max, pixels, opts, dygraph, vals) -> [{v: ..., label: ...}, ...]", "desc": "This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result. See dygraph-tickers.js for an extensive discussion. This is set on a per-axis basis."}, {"category": "Axes", "name": "valueRange", "tags": ["axes"], "default": "Full range of the input is shown", "examples": ["drawing", "dynamic-update", "is-zoomed-ignore-programmatic-zoom", "no-visibility", "reverse-y-axis", "synchronize", "zoom", "two-axes-vr"], "type": "Array of two numbers", "desc": "Explicitly set the vertical range of the graph to [low, high]. This may be set on a per-axis basis to define each y-axis separately."}, {"category": "Axes", "name": "xAxisHeight", "tags": ["axes"], "default": null, "type": "Integer", "desc": "Height, in pixels, of the x-axis. If not set explicitly, this is computed based on axisLabelFontSize and axisTickSize."}, {"category": "Axes", "name": "xAxisLabelWidth", "tags": ["axes"], "default": 50, "examples": ["x-axis-formatter", "value-axis-formatters"], "type": "Integer", "desc": "Width, in pixels, of the x-axis labels."}, {"category": "Axes", "name": "yAxisLabelWidth", "tags": ["axes"], "default": 50, "examples": ["customLabel", "two-axes", "multi-scale", "two-axes-vr", "value-axis-formatters"], "type": "Integer", "desc": "Width, in pixels, of the y-axis labels. This also affects the amount of space available for a y-axis chart label."}, {"category": "CSV parsing", "name": "delimiter", "tags": ["csv parsing"], "default": ",", "type": "String", "desc": "The delimiter to look for when separating fields of a CSV file. Setting this to a tab is not usually necessary, since tab-delimited data is auto-detected."}, {"category": "CSV parsing", "name": "xValueParser", "tags": ["csv parsing"], "default": "parseFloat() or Date.parse()*", "type": "function(str) -> number", "desc": "A function which parses x-values (i.e. the dependent series). Must return a number, even when the values are dates. In this case, millis since epoch are used. This is used primarily for parsing CSV data. *=Dygraphs is slightly more accepting in the dates which it will parse. See code for details."}, {"category": "Callbacks", "name": "clickCallback", "tags": ["callbacks"], "default": null, "examples": ["callback"], "type": "function(e, x, points)", "desc": "A function to call when the canvas is clicked. The function should take three arguments, the event object for the click, the x-value that was clicked (for dates this is millis since epoch), and the closest points along that date. The points have these properties:\n * xval/yval: The data coordinates of the point (with dates/times as millis since epoch) \n * canvasx/canvasy: The canvas coordinates at which the point is drawn. \n name: The name of the data series to which the point belongs"}, {"category": "Callbacks", "name": "drawCallback", "tags": ["callbacks"], "default": null, "examples": ["linear-regression-addseries", "annotation", "callback", "is-zoomed", "is-zoomed-ignore-programmatic-zoom", "synchronize", "zoom"], "type": "function(dygraph, is_initial)", "desc": "When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning. The first parameter is the dygraph being drawn. The second is a boolean value indicating whether this is the initial draw."}, {"category": "Callbacks", "name": "highlightCallback", "tags": ["callbacks"], "default": null, "examples": ["callback", "crosshair"], "type": "function(event, x, points,row)", "desc": "When set, this callback gets called every time a new point is highlighted. The parameters are the JavaScript mousemove event, the x-coordinate of the highlighted points and an array of highlighted points: [ {name: 'series', yval: y-value}, ... ]"}, {"category": "Callbacks", "name": "underlayCallback", "tags": ["callbacks"], "default": null, "examples": ["highlighted-region", "interaction", "linear-regression-fractions", "linear-regression", "underlay-callback"], "type": "function(canvas, area, dygraph)", "desc": "When set, this callback gets called before the chart is drawn. It details on how to use this."}, {"category": "Callbacks", "name": "unhighlightCallback", "tags": ["callbacks"], "default": null, "examples": ["callback", "crosshair"], "type": "function(event)", "desc": "When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph.  The parameter is the mouseout event."}, {"category": "Callbacks", "name": "zoomCallback", "tags": ["callbacks"], "default": null, "examples": ["callback", "is-zoomed-ignore-programmatic-zoom", "zoom"], "type": "function(minDate, maxDate, yRanges)", "desc": "A function to call when the zoom window is changed (either by zooming in or out). minDate and maxDate are milliseconds since epoch. yRanges is an array of [bottom, top] pairs, one for each y-axis."}, {"category": "Chart labels", "name": "title", "tags": ["chart labels"], "default": null, "examples": ["border", "demo", "noise", "styled-chart-labels", "multi-scale", "range-selector", "temperature-sf-ny"], "type": "String", "desc": "Text to display above the chart. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-title' classes."}, {"category": "Chart labels", "name": "titleHeight", "tags": ["chart labels"], "default": 18, "examples": ["styled-chart-labels"], "type": "Integer", "desc": "Height of the chart title, in pixels. This also controls the default font size of the title. If you style the title on your own, this controls how much space is set aside above the chart for the title's div."}, {"category": "Chart labels", "name": "xLabelHeight", "tags": ["chart labels"], "default": 18, "type": "Integer", "desc": "Height of the x-axis label, in pixels. This also controls the default font size of the x-axis label. If you style the label on your own, this controls how much space is set aside below the chart for the x-axis label's div."}, {"category": "Chart labels", "name": "xlabel", "tags": ["chart labels"], "default": null, "examples": ["border", "demo", "styled-chart-labels", "multi-scale"], "type": "String", "desc": "Text to display below the chart's x-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-xlabel' classes."}, {"category": "Chart labels", "name": "y2label", "tags": ["chart labels"], "default": null, "examples": ["two-axes", "two-axes-vr"], "type": "String", "desc": "Text to display to the right of the chart's secondary y-axis. This label is only displayed if a secondary y-axis is present. See this test for an example of how to do this. The comments for the 'ylabel' option generally apply here as well. This label gets a 'dygraph-y2label' instead of a 'dygraph-ylabel' class."}, {"category": "Chart labels", "name": "yLabelWidth", "tags": ["chart labels"], "default": 18, "type": "Integer", "desc": "Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."}, {"category": "Chart labels", "name": "ylabel", "tags": ["chart labels"], "default": null, "examples": ["border", "demo", &qu