DataSet UI now wired up properly, woo.
authordsc <dsc@wikimedia.org>
Mon, 7 May 2012 19:52:35 +0000 (12:52 -0700)
committerdsc <dsc@wikimedia.org>
Mon, 7 May 2012 19:52:35 +0000 (12:52 -0700)
data/graphs/ohai.json
lib/dataset/data-view.co
lib/dataset/dataset-model.co
lib/dataset/dataset-view.co
lib/dataset/metric-edit-view.co
lib/dataset/metric-model.co
lib/graph/graph-edit-view.co
lib/graph/graph-model.co
lib/template/metric-edit.jade

index c15f14c..f27e9dc 100644 (file)
@@ -1,118 +1 @@
-{
-    "id": "ohai",
-    "slug": "ohai",
-    "name": "ohai~",
-    "desc": "A graph for the testing of great justice.",
-    "notes": "",
-    "width": "auto",
-    "height": 250,
-    "parents": ["root"],
-    "dataset": "/data/datasources/rc/rc_page_requests.csv",
-    "data": {
-        "metrics": [{
-            "source_id": "rc_active_editors_count",
-            "source_col": 1,
-            "label": "Total Active Editors",
-            "color": "#E62F74"
-        }, {
-            "source_id": "rc_very_active_editors_count",
-            "source_col": 1,
-            "label": "Total Very Active Editors",
-            "color": "#244792"
-        }, {
-            "source_id": "rc_edits_count",
-            "source_col": 1,
-            "label": "Total Edits",
-            "color": "#FF6458"
-        }]
-    },
-    "chartType": "dygraphs",
-    "options": {
-        "animatedZooms": true,
-        "avoidMinZero": false,
-        "axis": null,
-        "axisLabelColor": "#666666",
-        "axisLabelFontSize": 11,
-        "axisLabelFormatter": null,
-        "axisLabelWidth": 50,
-        "axisLineColor": "#AAAAAA",
-        "axisLineWidth": 0.3,
-        "axisTickSize": 3,
-        "colorSaturation": 1,
-        "colorValue": 0.5,
-        "colors": ["#FF0097", "#EF8158", "#83BB32", "#182B53", "#4596FF", "#553DC9", "#AD3238", "#00FFBC", "#F1D950"],
-        "connectSeparatedPoints": false,
-        "customBars": false,
-        "dateWindow": null,
-        "delimiter": ",",
-        "digitsAfterDecimal": 2,
-        "displayAnnotations": false,
-        "drawPoints": true,
-        "drawXAxis": true,
-        "drawXGrid": true,
-        "drawYAxis": true,
-        "drawYGrid": true,
-        "errorBars": false,
-        "file": null,
-        "fillAlpha": 0.15,
-        "fillGraph": false,
-        "fractions": false,
-        "gridLineColor": "#D8D8D8",
-        "gridLineWidth": 0.3,
-        "hideOverlayOnMouseOut": false,
-        "highlightCircleSize": 4,
-        "includeZero": false,
-        "interactionModel": null,
-        "isZoomedIgnoreProgrammaticZoom": false,
-        "labels": null,
-        "labelsDiv": null,
-        "labelsDivStyles": null,
-        "labelsDivWidth": 250,
-        "labelsKMB": true,
-        "labelsKMG2": false,
-        "labelsSeparateLines": true,
-        "labelsShowZeroValues": true,
-        "legend": "always",
-        "logscale": true,
-        "maxNumberWidth": 30,
-        "panEdgeFraction": null,
-        "pixelsPerLabel": null,
-        "pixelsPerXLabel": null,
-        "pixelsPerYLabel": null,
-        "pointSize": 1,
-        "rangeSelectorHeight": 40,
-        "rangeSelectorPlotFillColor": "#A7B1C4",
-        "rangeSelectorPlotStrokeColor": "#808FAB",
-        "rightGap": 20,
-        "rollPeriod": 1,
-        "showLabelsOnHighlight": true,
-        "showRangeSelector": false,
-        "showRoller": false,
-        "sigFigs": null,
-        "sigma": 2,
-        "stackedGraph": false,
-        "stepPlot": false,
-        "strokePattern": null,
-        "strokeWidth": 4,
-        "ticker": null,
-        "title": null,
-        "titleHeight": 18,
-        "valueFormatter": null,
-        "valueRange": null,
-        "visibility": null,
-        "wilsonInterval": true,
-        "xAxisHeight": null,
-        "xAxisLabelFormatter": null,
-        "xAxisLabelWidth": 55,
-        "xLabelHeight": 18,
-        "xValueFormatter": null,
-        "xValueParser": null,
-        "xlabel": null,
-        "y2label": null,
-        "yAxisLabelFormatter": null,
-        "yAxisLabelWidth": 50,
-        "yLabelWidth": 18,
-        "yValueFormatter": null,
-        "ylabel": null
-    }
-}
+{"options":{"animatedZooms":true,"avoidMinZero":false,"axis":null,"axisLabelColor":"#666666","axisLabelFontSize":11,"axisLabelFormatter":null,"axisLabelWidth":50,"axisLineColor":"#AAAAAA","axisLineWidth":0.3,"axisTickSize":3,"colorSaturation":1,"colorValue":0.5,"colors":["#FF0097","#EF8158","#83BB32","#182B53","#4596FF","#553DC9","#AD3238","#00FFBC","#F1D950"],"connectSeparatedPoints":false,"customBars":false,"dateWindow":null,"delimiter":",","digitsAfterDecimal":2,"displayAnnotations":false,"drawPoints":true,"drawXAxis":true,"drawXGrid":true,"drawYAxis":true,"drawYGrid":true,"errorBars":false,"file":null,"fillAlpha":0.15,"fillGraph":false,"fractions":false,"gridLineColor":"#D8D8D8","gridLineWidth":0.3,"hideOverlayOnMouseOut":false,"highlightCircleSize":4,"includeZero":false,"interactionModel":null,"isZoomedIgnoreProgrammaticZoom":false,"labels":null,"labelsDiv":null,"labelsDivStyles":null,"labelsDivWidth":250,"labelsKMB":true,"labelsKMG2":false,"labelsSeparateLines":true,"labelsShowZeroValues":true,"legend":"always","logscale":true,"maxNumberWidth":30,"panEdgeFraction":null,"pixelsPerLabel":null,"pixelsPerXLabel":null,"pixelsPerYLabel":null,"pointSize":1,"rangeSelectorHeight":40,"rangeSelectorPlotFillColor":"#A7B1C4","rangeSelectorPlotStrokeColor":"#808FAB","rightGap":20,"rollPeriod":1,"showLabelsOnHighlight":true,"showRangeSelector":false,"showRoller":false,"sigFigs":null,"sigma":2,"stackedGraph":false,"stepPlot":false,"strokePattern":null,"strokeWidth":4,"ticker":null,"title":null,"titleHeight":18,"valueFormatter":null,"valueRange":null,"visibility":null,"wilsonInterval":true,"xAxisHeight":null,"xAxisLabelFormatter":null,"xAxisLabelWidth":55,"xLabelHeight":18,"xValueFormatter":null,"xValueParser":null,"xlabel":null,"y2label":null,"yAxisLabelFormatter":null,"yAxisLabelWidth":50,"yLabelWidth":18,"yValueFormatter":null,"ylabel":null},"slug":"ohai","name":"ohai~","desc":"A graph for the testing of great justice.","notes":"","width":"auto","height":250,"chartType":"dygraphs","parents":["root"],"id":"ohai","data":{"palette":null,"lines":[],"id":"ohai","metrics":[{"index":0,"label":"Total Active Editors","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_active_editors_count","source_col":1,"color":"#E62F74","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1},{"index":0,"label":"Total Very Active Editors","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_very_active_editors_count","source_col":1,"color":"#244792","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1},{"index":0,"label":"","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_new_article_count","source_col":1,"color":"#FF6458","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1,"source-url":"/data/datasources/rc/rc_very_active_editors_count.csv"},{"index":3,"label":"New Editors","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_new_editors_count","source_col":1,"color":"rgb(253,174,97)","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1,"source-url":"/data/datasources/rc/rc_very_active_editors_count.csv"},{"index":4,"label":"","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_binary_files","source_col":10,"color":"rgb(254,224,139)","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1}]},"dataset":"/data/datasources/rc/rc_page_requests.csv"}
\ No newline at end of file
index d36cc61..b947e9a 100644 (file)
@@ -47,7 +47,7 @@ DataView = exports.DataView = BaseView.extend do # {{{
         @addSubview @dataset_view
             .on 'add-metric',       @onMetricsChanged,  this
             .on 'remove-metric',    @onMetricsChanged,  this
-            .on 'edit-metric',      @editMetric,        this
+            .on 'select-metric',    @selectMetric,      this
         
         @render()
         @triggerReady()
@@ -87,6 +87,7 @@ DataView = exports.DataView = BaseView.extend do # {{{
             .on 'metric-update', @onUpdateMetric, this
             .on 'metric-change', @onUpdateMetric, this
         @metric_views.push @addSubview view
+        @renderSubviews()
         metric
     
     removeMetric: (metric) ->
@@ -96,14 +97,15 @@ DataView = exports.DataView = BaseView.extend do # {{{
         @removeSubview view
         metric
     
-    editMetric: (metric) ->
-        console.log "#this.editMetric!", metric
+    selectMetric: (metric) ->
+        # console.log "#this.selectMetric!", metric
         @metric_views.invoke 'hide'
         @metric_edit_view = @metric_views.findByModel metric
         @metric_edit_view?.show()
         _.delay @onMetricsChanged, 10
     
     onMetricsChanged: ->
+        return unless @dataset_view
         oldMinHeight = parseInt @$el.css 'min-height'
         newMinHeight = Math.max do
             @dataset_view.$el.height()
index 5d0fa33..44e3a4c 100644 (file)
@@ -34,16 +34,16 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
         metrics : []
     
     
-    constructor: function DataSet
-        BaseModel ...
+    constructor: function DataSet (attributes={}, opts)
+        @metrics = new MetricList attributes.metrics
+        BaseModel.call this, attributes, opts
     
     initialize : ->
         BaseModel::initialize ...
-        @metrics = new MetricList @attributes.metrics
+        @set 'metrics', @metrics, {+silent}
         @on 'change:metrics', @onMetricChange, this
     
     
-    
     load: (opts={}) ->
         @resetReady() if opts.force
         return this if @loading or @ready
@@ -67,6 +67,34 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
                 @triggerReady()
         this
     
+    # refreshSubModels: ->
+    #     # @set 'metrics', @metrics.toJSON(), {+silent}
+    #     @set 'metrics', _.pluck(@metrics.models, 'attributes'), {+silent}
+    #     this
+    
+    /**
+     * Override to handle the case where one of our rich sub-objects is attempted
+     * to be overridden with a native object.
+     */
+    set: (key, value, opts) ->
+        # return DataSet.__super__.set ... unless @metrics
+        
+        if _.isObject(key) and key?
+            [values, opts] = [key, value]
+        else
+            values = { "#key": value }
+        opts or= {}
+        
+        for key, value in values
+            continue unless key is 'metrics' and _.isArray value
+            @metrics.reset value
+            delete values[key]
+            unless opts.silent
+                DataSet.__super__.set.call this, 'metrics', value, {+silent}
+                DataSet.__super__.set.call this, 'metrics', @metrics, opts
+        
+        DataSet.__super__.set.call this, values, opts
+    
     
     /* * * *  TimeSeriesData interface  * * * {{{ */
     
@@ -74,18 +102,21 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
      * @returns {Array<Array>} The reified dataset, materialized to a list of rows including timestamps.
      */
     getData: ->
+        return [] unless @ready
         _.zip ...@getColumns()
     
     /**
      * @returns {Array<Array>} List of all columns (including date column).
      */
     getColumns: ->
-        [ @getDateColumn() ].concat @getDataColumns()
+        return [] unless @ready
+        _.compact [ @getDateColumn() ].concat @getDataColumns()
     
     /**
      * @returns {Array<Date>} The date column.
      */
     getDateColumn: ->
+        return [] unless @ready
         dates = @metrics.onlyOk().invoke 'getDateColumn'
         maxLen = _.max _.pluck dates, 'length'
         _.find dates, -> it.length is maxLen
@@ -94,33 +125,45 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{
      * @returns {Array<Array>} List of all columns except the date column.
      */
     getDataColumns: ->
+        return [] unless @ready
         @metrics.onlyOk().invoke 'getData'
     
     /**
      * @returns {Array<String>} List of column labels.
      */
     getLabels: ->
+        return [] unless @ready
         [ 'Date' ].concat @metrics.onlyOk().invoke 'getLabel'
     
-    # }}}
-    
     getColors: ->
+        return [] unless @ready
         @metrics.onlyOk().pluck 'color'
     
+    # }}}
+    
+    
     newMetric: ->
         index = @metrics.length
         @metrics.add m = new Metric { index, color:ColorBrewer.Spectral[11][index] }
+        # @get 'metrics' .push m.attributes
+        # @trigger 'change:metrics',  this, @metrics, 'metrics'
+        # @trigger 'change',          this, @metrics, 'metrics'
         m
     
     onMetricChange: ->
         console.log "#this.onMetricChange! ready=#{@ready}"
         @resetReady()
-        @metrics.reset @get 'metrics'
         @load()
     
-    # TODO: toJSON() must ensure columns in MetricList are ordered by index
+    
+    # XXX: toJSON() must ensure columns in MetricList are ordered by index
     #   ...in theory, MetricList.comparator now does this
     
+    # toJSON: ->
+    #     @refreshSubModels()
+    #     json = DataSet.__super__.toJSON ...
+    #     json.metrics = json.metrics.map -> it.toJSON?() or it
+    #     json
     
 # }}}
 
index e19efcb..34d7ff7 100644 (file)
@@ -14,7 +14,7 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
     
     events:
         'click .new-metric-button'       : 'newMetric'
-        'click .metrics .dataset-metric' : 'editMetric'
+        'click .metrics .dataset-metric' : 'selectMetric'
     
     views_by_cid : {}
     active_view : null
@@ -43,25 +43,24 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
     
     addMetric: (metric) ->
         console.log "#this.addMetric!", metric
-        if metric.view
-            @removeSubview metric.view.remove()
+        if @views_by_cid[metric.cid]
+            @removeSubview that
             delete @views_by_cid[metric.cid]
         
         view = @addSubview new DataSetMetricView {model:metric, @graph_id}
         @views_by_cid[metric.cid] = view
-        @$ '.metrics' .append view.render().el
-        
-        # @render()
+        # @$ '.metrics' .append view.render().el
         @trigger 'add-metric', metric, view, this
+        @render()
         view
     
     removeMetric: (metric) ->
         console.log "#this.removeMetric!", metric
-        if view = metric.view
-            @removeSubview view.remove()
+        if view = @views_by_cid[metric.cid]
+            @removeSubview view
             delete @views_by_cid[metric.cid]
             @trigger 'remove-metric', metric, view, this
-        metric.view
+        view
     
     addAllMetrics: ->
         console.log "#this.addAllMetrics! --> #{@model.metrics}"
@@ -69,18 +68,17 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{
         @model.metrics.each @addMetric, this
         this
     
-    editMetric: (metric) ->
-        # console.log "#this.editMetric!", metric
+    selectMetric: (metric) ->
         if metric instanceof [jQuery.Event, Event]
             metric = $ metric.currentTarget .data 'model'
         view = @active_view = @views_by_cid[metric.cid]
-        console.log "#this.editMetric!", metric
+        # console.log "#this.selectMetric!", metric
         
         @$ '.metrics .dataset-metric' .removeClass 'metric-active'
         view.$el.addClass 'metric-active'
         view.$el.find '.activity-arrow' .css 'font-size', 2+view.$el.height()
         
-        @trigger 'edit-metric', metric, view, this
+        @trigger 'select-metric', metric, view, this
         this
     
     
@@ -120,8 +118,8 @@ DataSetMetricView = exports.DataSetMetricView = BaseView.extend do # {{{
                 'disabled' if m.disabled,
             ]).map( -> "metric-#it" ).join ' '
             source :
-                if m.source_id and m.source_col_name
-                    "#{m.source_id}.#{m.source_col_name}"
+                if m.source_id and m.source_col
+                    "#{m.source_id}[#{m.source_col}]"
                 else
                     'No source'
             timespan :
index f0d787c..4b5be60 100644 (file)
@@ -52,6 +52,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
     
     update: ->
         MetricEditView.__super__.update ...
+        @$ '.metric-label' .attr 'placeholder', @model.getPlaceholderLabel()
         
         # Update the color picker
         @$ '.color-swatch'
@@ -85,6 +86,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{
     
     onSourceMetricChange: (metric) ->
         console.log "#this.onSourceMetricChange!", metric
+        @$ '.metric-label' .attr 'placeholder', @model.getPlaceholderLabel()
         @trigger 'metric-change', @model, this
         this
     
index e3b6ce6..7083738 100644 (file)
@@ -64,7 +64,7 @@ Metric = exports.Metric = BaseModel.extend do # {{{
     
     getPlaceholderLabel: ->
         col  = @get 'source_col'
-        name = "#{@source.get 'shortName'}, #{@source.getColumnName col}" if @source and col > 0
+        name = "#{@source.get 'shortName'}, #{@source.getColumnName col}" if @source and col >= 0
         name or @NEW_METRIC_LABEL
     
     getSourceColumnName: ->
@@ -73,9 +73,12 @@ Metric = exports.Metric = BaseModel.extend do # {{{
     
     
     load: (opts={}) ->
-        source_id = @get 'source_id'
+        source_id  = @get 'source_id'
         @resetReady() if opts.force or @source?.id is not source_id
-        return this if not source_id or @loading or @ready
+        return this if @loading or @ready
+        
+        unless source_id and @get('source_col') >= 0
+            return @triggerReady()
         
         console.log "#this.load()..."
         @updateId()
index 54656cf..eec0146 100644 (file)
@@ -104,7 +104,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         
         
         ### Graph Data UI
-        @data_view = @addSubview new DataView { model:@model.get('data'), graph_id:@id }
+        @data_view = @addSubview new DataView { model:@model.dataset, graph_id:@id }
         @data_view
             .on 'start-waiting', @wait,         this
             .on 'stop-waiting',  @unwait,       this
@@ -275,7 +275,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
                     valueFormatter     : @numberFormatterHTML
         
         # console.log "#this.render!", dataset
-        _.dump options, 'options'
+        # _.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.
@@ -308,7 +308,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         # @renderDetails()
         # @attachSubviews()
         # _.invoke @subviews, 'render'
-        BaseView::render ...
+        GraphEditView.__super__.render ...
         @renderChart()
         # @updateURL()
         @trigger 'render', this
@@ -321,11 +321,11 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
      * Update the page URL using HTML5 History API
      */
     updateURL: ->
-        data  = @toJSON()
+        json  = @toJSON()
         title = "#{@model.get('name') or 'New Graph'} | Edit Graph | GraphKit"
         url   = @toURL('edit')
-        # console.log 'History.pushState', JSON.stringify(data), title, url
-        History.pushState data, title, url
+        # console.log 'History.pushState', JSON.stringify(json), title, url
+        History.pushState json, title, url
     
     
     ### }}}
@@ -397,7 +397,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
         #     \toptions: #{JSON.stringify options}
         #     \t^opts: #{JSON.stringify _.intersection _.keys(changes), _.keys(options)}
         # """
-        @chart?.updateOptions file:that if changes?.dataset
+        # @chart?.updateOptions file:that if changes?.dataset
         @chartOptions options, {+silent} if changes?.options
     
     onScaffoldChange: (scaffold, value, key, field) ->
@@ -435,8 +435,8 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
     
     onDetailsSubmit: ->
         console.log "#this.onDetailsSubmit!"
-        data = @$ 'form.graph-details' .formData()
-        @model.set data
+        details = @$ 'form.graph-details' .formData()
+        @model.set details
         false
     
     onOptionsSubmit: ->
index 8790325..2d231f5 100644 (file)
@@ -98,6 +98,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         
         # Insert submodels in place of JSON
         @dataset = new DataSet {id:@id, ...@get 'data'}
+            # .on 'change', @onDataSetChange, this
         @set 'data', @dataset, {+silent}
         
         @trigger 'init', this
@@ -121,9 +122,9 @@ Graph = exports.Graph = BaseModel.extend do # {{{
                         next.ok()
                     success : @unwaitAnd (model, res) ~>
                         # console.log "#{this}.fetch() --> success!", res
+                        # Update the DataSet model with the new values
                         @dataset.set @get 'data'
-                        @trigger 'change:data', this, @dataset, 'data'
-                        @trigger 'change',      this, @dataset, 'data'
+                        @set 'data', @dataset, {+silent}
                         next.ok res
             
             # Load Parents...
@@ -166,9 +167,11 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         Seq @dataset.metrics.models
             .parEach_ (next, metric) ->
                 metric.once 'ready', next.ok .load()
-            .parEach_ (next, metric) ->
-                metric.source
-                    .on 'load-data-success', next.ok .loadData()
+            .parEach_ (next, metric) ~>
+                unless metric.source
+                    console.warn "#{this}.loadData() -- Skipping metric #metric with invalid source!", metric
+                    return next.ok()
+                metric.source.on 'load-data-success', next.ok .loadData()
             .seq ~>
                 console.log "#{this}.loadData() complete!"
                 @loading = false
@@ -176,6 +179,9 @@ Graph = exports.Graph = BaseModel.extend do # {{{
                 @triggerReady 'dataReady', 'data-ready'
         this
     
+    onDataSetChange: ->
+        console.log "#this.onDataSetChange!"
+        @set 'data', @dataset, {+silent}
     
     
     ### Accessors
@@ -184,7 +190,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         if _.startsWith key, 'options.'
             @getOption key.slice(8)
         else
-            (@..__super__ or BaseModel::).get.call this, key
+            Graph.__super__.get.call this, key
     
     
     set: (key, value, opts) ->
@@ -195,7 +201,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
             values = { "#key": value }
         values = @parse values
         
-        setter = (@..__super__ or BaseModel::).set
+        setter = Graph.__super__.set
         
         # Merge options in, firing granulated change events
         if values.options
@@ -215,7 +221,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{
     
     
     
-    ### Chart Option Accessors ###
+    ### Chart Option Accessors {{{
     
     hasOption: (key) ->
         @getOption(key) is void
@@ -266,8 +272,8 @@ Graph = exports.Graph = BaseModel.extend do # {{{
         options
     
     
-    
-    ### Serialization
+    # }}}
+    ### Serialization {{{
     
     parse: (data) ->
         data = JSON.parse data if typeof data is 'string'
@@ -300,8 +306,8 @@ Graph = exports.Graph = BaseModel.extend do # {{{
     toJSON: (opts={}) ->
         opts = {+keepDefaults, +keepUnchanged} import opts
         # use jQuery's deep-copy implementation -- XXX: Deep-copy no longer necessary thanks to @getOptions()
-        # json = $.extend true, {}, @attributes
         json = _.clone(@attributes) import { options:@getOptions(opts) }
+        # { data: ...json }
     
     
     toKVPairs: (opts={}) ->
@@ -346,6 +352,8 @@ Graph = exports.Graph = BaseModel.extend do # {{{
     toPermalink: ->
         "#{root.location.protocol}//#{window.location.host}#{@toLink()}"
     
+    # }}}
+    
 
 ### Graph Cache for parent-lookup
 new ModelCache Graph
index 8b2a9d3..a95e2a7 100644 (file)
@@ -7,7 +7,7 @@ section.metric-edit-ui
                 input.metric-color(type='hidden', id="#{graph_id}_metric", name="color", value=color)
             .color-swatch.input-append.color(data-color="#{color}", data-color-format="hex")
                 input.metric-color(type='hidden', id="#{graph_id}_metric_color", name="color", value=color)
-                span.add-on: i(style="background-color: #{color};")
+                span.add-on: i(style="background-color: #{color};", title="#{color}")
             
             input.metric-label(type='text', id="#{graph_id}_metric_label", name='label', placeholder='#{placeholder_label}', value=label)