From f94e5d3c46303821cb87fea6a9b4bcc0a91046be Mon Sep 17 00:00:00 2001 From: dsc Date: Wed, 16 May 2012 00:28:12 -0700 Subject: [PATCH] Refactors much of GraphEditView and GraphDisplayView into a common base. --- lib/base/base-view.co | 2 + lib/graph/graph-display-view.co | 260 +++------------------------- lib/graph/graph-edit-view.co | 325 ++---------------------------------- lib/graph/graph-view.co | 353 +++++++++++++++++++++++++++++++++++++++ lib/graph/index.co | 3 +- lib/template/graph-edit.jade | 2 +- www/css/graph.styl | 4 +- www/modules.yaml | 1 + 8 files changed, 407 insertions(+), 543 deletions(-) create mode 100644 lib/graph/graph-view.co diff --git a/lib/base/base-view.co b/lib/base/base-view.co index dc9f0d0..a8d3ec9 100644 --- a/lib/base/base-view.co +++ b/lib/base/base-view.co @@ -209,12 +209,14 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ this render: -> + @wait() if @isBuilt @update() else @build() @renderSubviews() @trigger 'render', this + @unwait() this renderSubviews: -> diff --git a/lib/graph/graph-display-view.co b/lib/graph/graph-display-view.co index 9ccc38e..eb34859 100644 --- a/lib/graph/graph-display-view.co +++ b/lib/graph/graph-display-view.co @@ -2,32 +2,19 @@ moment = require 'moment' { _, op, } = require 'kraken/util' -{ BaseView, -} = require 'kraken/base' { Graph, } = require 'kraken/graph/graph-model' +{ GraphView, +} = require 'kraken/graph/graph-view' root = do -> this -DEBOUNCE_RENDER = 100ms /** * @class View for a graph visualization encapsulating. */ -GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ - 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 ]> - +GraphDisplayView = exports.GraphDisplayView = GraphView.extend do # {{{ tagName : 'section' className : 'graph graph-display' template : require 'kraken/template/graph-display' @@ -38,8 +25,6 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ 'click .export-button' : 'exportChart' # 'click .load-button' : 'load' - data : {} - ready : false constructor: function GraphDisplayView @@ -47,238 +32,57 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ initialize : (o={}) -> @data = {} - @model or= new Graph - @id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid) - BaseView::initialize ... + GraphDisplayView.__super__.initialize ... # console.log "#this.initialize!" - for name of @__debounce__ - @[name] = _.debounce @[name], DEBOUNCE_RENDER - - - ### 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 - - _.delay (~> @onReady()), 100 if @model.ready + @loadData() + onReady: -> + return if @ready + @triggerReady() + @onSync() - load: -> - console.log "#this.load!" - @model.fetch() - false + ### Rendering {{{ - change: -> - @model.change() + update: -> + locals = @toTemplateLocals() + @$ '.graph-name a' .text(locals.name or '') + @$ '.graph-desc' .html jade.filters.markdown locals.desc or '' 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 = @$ '.viewport' - viewport.attr 'style', '' - label = @$ '.graph-legend' - - if width is 'auto' - vpWidth = viewport.innerWidth() or 300 - labelW = label.outerWidth() or 228 - width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW) - width ?= modelW - if height is 'auto' - height = viewport.innerHeight() or 320 - 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() - viewport = @$ '.viewport' - - # XXX: use @model.changedAttributes() to calculate what to update - options = @chartOptions() #import size - options import do - labelsDiv : @$ '.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 - + render: -> + return this unless @ready and not @isRendering + @wait() + @checkWaiting() + root.title = "#{@get 'name'} | GraphKit" + GraphDisplayView.__super__.render ... + @unwait() + @checkWaiting() + @isRendering = false this - /** * Exports graph as png */ exportChart: (evt) -> # The following code is dygraph specific, thus should not - # be implemented in this class. Rather in a dygraph charttype - # related class. The same is true for the 'renderChart' method above. - + # be implemented in this class. Rather in the Dygraphs ChartType-subclass. + # The same is true for the 'renderChart' method above. + # # The Dygraph.Export module is from http://cavorite.com/labs/js/dygraphs-export/ - # Todo: We don't use the tile of the chart, which is thus missing from the png + # TODO: We don't use the title of the chart, which is thus missing from the png. console.log "#this.export!" img = @$el.find '.export-image' - Dygraph.Export.asPNG(@chart, img); + Dygraph.Export.asPNG @chart, img window.open img.src, "toDataURL() image" - - - - update: -> - locals = @toTemplateLocals() - @$ '.graph-name a' .text(locals.name or '') - @$ '.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 - - - ### 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 - """ - #whole#fraction#suffix - """ ### }}} ### 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 - /** * Selects the graph permalink input field. */ @@ -288,14 +92,6 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ # defer, the focusing click will # unselect the text. _.defer( ~> @$ '.graph-permalink input' .select() ) - - - - # Needed because (sigh) _.debounce returns undefined - stopAndRender: -> - @render ... - false - # }}} # }}} diff --git a/lib/graph/graph-edit-view.co b/lib/graph/graph-edit-view.co index 37b44a8..05bbbac 100644 --- a/lib/graph/graph-edit-view.co +++ b/lib/graph/graph-edit-view.co @@ -1,12 +1,12 @@ moment = require 'moment' _ = require 'kraken/util/underscore' -{ BaseView, -} = require 'kraken/base' -{ ChartOptionScaffold, DEBOUNCE_RENDER, -} = require 'kraken/chart' { Graph, } = require 'kraken/graph/graph-model' +{ GraphView, +} = require 'kraken/graph/graph-view' +{ ChartOptionScaffold, DEBOUNCE_RENDER, +} = require 'kraken/chart' { DataView, DataSetView, DataSet, } = require 'kraken/dataset' @@ -20,20 +20,8 @@ root = do -> this * - Graph metadata, such as name, description, slug * - Chart options, using ChartOptionScaffold */ -GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ - FILTER_CHART_OPTIONS : <[ - file labels visibility colors dateWindow ticker timingName xValueParser - axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter - valueFormatter xValueFormatter yValueFormatter - ]> - __bind__ : <[ - render stopAndRender resizeViewport wait unwait checkWaiting - numberFormatter numberFormatterHTML - onReady onSync onModelChange onScaffoldChange - onFirstClickRenderOptionsTab onFirstClickRenderDataTab - ]> - __debounce__: <[ render ]> - tagName : 'section' +GraphEditView = exports.GraphEditView = GraphView.extend do # {{{ + __bind__ : <[ wait unwait onScaffoldChange onFirstClickRenderOptionsTab onFirstClickRenderDataTab ]> className : 'graph-edit graph' template : require 'kraken/template/graph-edit' @@ -50,17 +38,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ 'change .chart-options input[type="checkbox"]' : 'onOptionsSubmit' - /** - * Count of outstanding tasks until we stop the spinner. - * @type Number - */ - waitingOn : 0 - - /** - * Whether we're ready. - * @type Boolean - */ - ready : false @@ -68,34 +45,12 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ BaseView ... initialize : (o={}) -> - @model or= new Graph - @id = @graph_id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid) - BaseView::initialize ... + GraphEditView.__super__.initialize ... # console.log "#this.initialize!" - for name of @__debounce__ - @[name] = _.debounce @[name], DEBOUNCE_RENDER - - # Set up the spinner - @on 'start-waiting', @onStartWaiting, this - @on 'stop-waiting', @onStopWaiting, this - @onStartWaiting() if @waitingOn # In case we missed the first call to @wait() somehow - # Start a wait for the `ready` event @wait() - ### Model Events - @model - .on 'start-waiting', @wait, this - .on 'stop-waiting', @unwait, this - .on 'sync', @onSync, this - .on 'destroy', @remove, this - .on 'change', @render, this - .on 'change:dataset', @onModelChange, this - .on 'change:options', @onModelChange, this - .on 'error', @onModelError, this - # .on 'ready', @onReady, this - ### Chart Options Tab, Scaffold @scaffold = @addSubview new ChartOptionScaffold @scaffold.collection.reset that if o.graph_spec @@ -110,28 +65,14 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ .on 'stop-waiting', @unwait, this .on 'metric-change', @onDataChange, this - # Rerender once the tab is visible # Can't use @events because we need to bind before registering @$el.on 'click', '.graph-data-tab', @onFirstClickRenderDataTab @$el.on 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab - ### 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 - # Kick off model load chain - @checkWaiting() - Seq() - .seq_ (next) ~> - @model.once 'ready', next.ok .load() - .seq_ (next) ~> - @model.once 'data-ready', next.ok .loadData() - .seq ~> @onReady() + @loadData() onReady: -> return if @ready @@ -151,22 +92,15 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ ### Persistence {{{ - load: -> - console.log "#this.load!" - @wait() - @model.fetch { success:@unwait, error:@unwait } - false - - save: -> - console.log "#this.save!" - @wait() - id = @model.get('slug') or @model.id - @model.save {id}, { +wait, success:@unwait, error:@unwait } - false - + /** + * Save the graph and return to the graph viewer/browser. + */ done: -> @save() + /** + * Flush all changes. + */ change: -> @model.change() @scaffold.invoke 'change' @@ -195,125 +129,20 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ delete options[k] options - toTemplateLocals: -> - attrs = _.clone @model.attributes - delete attrs.options - { @model, view:this, @graph_id, slug:'', name:'', desc:'' } 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 - - viewport = @$ '.viewport' - - # Remove old style, as it confuses dygraph after options update - viewport.attr 'style', '' - label = @$ '.graph-label' - - if width is 'auto' - vpWidth = viewport.innerWidth() or 300 - labelW = label.outerWidth() or 228 - width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW) - width ?= modelW - if height is 'auto' - height = viewport.innerHeight() or 320 - height ?= modelH - - size = { width, height } - viewport.css size - # console.log 'resizeViewport!', JSON.stringify(size), viewport - # @chart.resize size if forceRedraw - size - - - # Repopulate UI from Model - renderDetails: -> - form = @$ 'form.graph-details' - for k, v in @model.attributes - continue if k is 'options' - txt = @model.serialize v - - el = form.find "input[name=#k]" - if el.attr('type') is 'checkbox' - el.attr 'checked', if v then 'checked' else '' - else - el.val txt - - form.find "textarea[name=#k]" .text txt - - # Graph Name field is not part of the form due to the layout. - @$ "input.graph-name[name='name']" .val @get 'name' - this - - # Redraw chart inside viewport. - renderChart: -> - # data = @model.get 'dataset' - # data = data.getData() if typeof data is not 'string' - dataset = @model.dataset - data = dataset.getData() - size = @resizeViewport() - - # XXX: use @model.changedAttributes() to calculate what to update - options = @chartOptions() #import size - options import do - colors : dataset.getColors() - labels : dataset.getLabels() - labelsDiv : @$ '.graph-label' .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 - attachSubviews: -> - BaseView::attachSubviews ... + GraphEditView.__super__.attachSubviews ... @checkWaiting() render: -> - return this unless @ready and not @_rendering - @_rendering = true + return this unless @ready and not @isRendering @wait() @checkWaiting() - # @renderDetails() - # @attachSubviews() - # _.invoke @subviews, 'render' + root.title = "#{@get 'name'} | GraphKit" GraphEditView.__super__.render ... @renderChart() # @updateURL() - @trigger 'render', this @unwait() - @_rendering = false + @isRendering = false this @@ -329,77 +158,8 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ ### }}} - ### 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 - # coco will trim all the whitespace - " - #whole - #fraction - #suffix - " - - ### }}} ### Event Handlers {{{ - onSync: -> - return unless @ready - console.info "#this.sync() --> success!" - # TODO: UI alert - @chartOptions @model.getOptions(), {+silent} - @render() - - onStartWaiting: -> - status = @checkWaiting() - # console.log "#this.onStartWaiting!", status - - onStopWaiting: -> - status = @checkWaiting() - # console.log "#this.onStopWaiting!", status - - onModelError: -> - console.error "#this.error!", arguments - # TODO: UI alert - - onModelChange: -> - changes = @model.changedAttributes() - options = @model.getOptions() - # console.log """ - # Graph.changed( options ) -> - # \tchanges: #{JSON.stringify changes} - # \toptions: #{JSON.stringify options} - # \t^opts: #{JSON.stringify _.intersection _.keys(changes), _.keys(options)} - # """ - # @chart?.updateOptions file:that if changes?.dataset - @chartOptions options, {+silent} if changes?.options - onScaffoldChange: (scaffold, value, key, field) -> current = @model.getOption(key) # console.log do @@ -444,55 +204,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ @render() false - # Needed because (sigh) _.debounce returns undefined, and we need to preventDefault() - stopAndRender : -> @render ... ; false - - # }}} - ### Spinner {{{ - - /** - * Retrieve or construct the spinner. - */ - spinner: -> - el = @$ '.graph-spinner' - unless el.data 'spinner' - ### Spin.js Options ### - opts = - lines : 9 # [12] The number of lines to draw - length : 2 # [7] The length of each line - width : 1 # [5] The line thickness - radius : 7 # [10] The radius of the inner circle - rotate : -10.5 # [0] rotation offset - trail : 50 # [100] Afterglow percentage - opacity : 1/4 # [1/4] Opacity of the lines - shadow : false # [false] Whether to render a shadow - speed : 1 # [1] Spins per second - zIndex : 2e9 # [2e9] zIndex; uses a very high z-index by default - color : '#000' # ['#000'] Line color; '#rgb' or '#rrggbb'. - top : 'auto' # ['auto'] Top position relative to parent in px; 'auto' = center vertically. - left : 'auto' # ['auto'] Left position relative to parent in px; 'auto' = center horizontally. - className : 'spinner' # ['spinner'] CSS class to assign to the element - fps : 20 # [20] Frames per second when falling back to `setTimeout()`. - hwaccel : Modernizr.csstransforms3d # [false] Whether to use hardware acceleration. - - isHidden = el.css('display') is 'none' - el.show().spin opts - el.hide() if isHidden - el - - checkWaiting: -> - spinner = @spinner() - if isWaiting = (@waitingOn > 0) - spinner.show() - if spinner.find('.spinner').css('top') is '0px' - # delete spinner - spinner.spin(false) - # re-add to DOM with correct parent sizing - @spinner() - else - spinner.hide() - isWaiting - # }}} # }}} diff --git a/lib/graph/graph-view.co b/lib/graph/graph-view.co new file mode 100644 index 0000000..4ff25d3 --- /dev/null +++ b/lib/graph/graph-view.co @@ -0,0 +1,353 @@ +moment = require 'moment' + +_ = require 'kraken/util/underscore' +{ BaseView, +} = require 'kraken/base' +{ Graph, +} = require 'kraken/graph/graph-model' + +root = do -> this +DEBOUNCE_RENDER = 100ms + + + + +/** + * @class Base view for a Graph visualizations. + */ +GraphView = exports.GraphView = BaseView.extend do # {{{ + FILTER_CHART_OPTIONS : <[ + file labels visibility colors dateWindow ticker timingName xValueParser + axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter + valueFormatter xValueFormatter yValueFormatter + ]> + __bind__ : <[ + render stopAndRender resizeViewport checkWaiting + numberFormatter numberFormatterHTML + onReady onSync onModelChange + ]> + __debounce__: <[ render ]> + tagName : 'section' + + + constructor: function GraphView + BaseView ... + + initialize : (o={}) -> + @model or= new Graph + @id = @graph_id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid) + GraphView.__super__.initialize ... + + for name of @__debounce__ + @[name] = _.debounce @[name], DEBOUNCE_RENDER + + # Set up the spinner + @on 'start-waiting', @onStartWaiting, this + @on 'stop-waiting', @onStopWaiting, this + @onStartWaiting() if @waitingOn # In case we missed the first call to @wait() somehow + + ### Model Events + @model + .on 'start-waiting', @wait, this + .on 'stop-waiting', @unwait, this + .on 'sync', @onSync, this + .on 'destroy', @remove, this + .on 'change', @render, this + .on 'change:dataset', @onModelChange, this + .on 'change:options', @onModelChange, this + .on 'error', @onModelError, this + + ### 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 + + loadData: -> + @resizeViewport() + @wait() + Seq() + .seq_ (next) ~> + @model.once 'ready', next.ok .load() + .seq_ (next) ~> + @model.once 'data-ready', next.ok .loadData() + .seq ~> + @unwait() + @onReady() + + + ### Persistence {{{ + + /** + * Reload the graph definition from the server. + */ + load: -> + console.log "#this.load!" + @wait() + @model.fetch { success:@unwait, error:@unwait } + false + + /** + * Save the graph definition to the server. + */ + save: -> + console.log "#this.save!" + @wait() + id = @model.get('slug') or @model.id + @model.save {id}, { +wait, success:@unwait, error:@unwait } + false + + /** + * Flush all changes. + */ + change: -> + @model.change() + this + + + ### Rendering {{{ + + chartOptions: (values, opts) -> + # Handle @chartOptions(k, v, opts) + if arguments.length > 1 and typeof values is 'string' + [k, v, opts] = arguments + values = { "#k": v } + values or= {} + + 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 + { @model, view:this, @graph_id, slug:'', name:'', desc:'' } 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 = @$ '.viewport' + viewport.attr 'style', '' + label = @$ '.graph-legend' + + if width is 'auto' + vpWidth = viewport.innerWidth() or 300 + labelW = label.outerWidth() or 228 + width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW) + width ?= modelW + if height is 'auto' + height = viewport.innerHeight() or 320 + 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' + # data = data.getData() if typeof data is not 'string' + dataset = @model.dataset + data = dataset.getData() + size = @resizeViewport() + + # XXX: use @model.changedAttributes() to calculate what to update? + options = @chartOptions() #import size + options import do + colors : dataset.getColors() + labels : dataset.getLabels() + labelsDiv : @$ '.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 + + + /** + * Render the chart and other Graph-derived view components. + */ + render: -> + return this unless @ready + @wait() + @checkWaiting() + GraphView.__super__.render ... + @renderChart() + @unwait() + @checkWaiting() + this + + + ### }}} + ### Formatters {{{ + + # XXX: Dygraphs-specific + axisFormatter: (fmttr) -> + (n, granularity, opts, g) -> fmttr n, opts, g + + # XXX: Dygraphs-specific + axisDateFormatter: (n, granularity, opts, g) -> + moment(n).format 'MM/YYYY' + + # XXX: Dygraphs-specific + 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 } + + # XXX: Dygraphs-specific + numberFormatter: (n, opts, g) -> + digits = opts('digitsAfterDecimal') ? 2 + { whole, fraction, suffix } = @_numberFormatter n, digits + "#whole#fraction#suffix" + + # XXX: Dygraphs-specific + numberFormatterHTML: (n, opts, g) -> + digits = opts('digitsAfterDecimal') ? 2 + { whole, fraction, suffix } = @_numberFormatter n, digits + # coco will trim the whitespace + " + #whole + #fraction + #suffix + " + + ### }}} + ### Event Handlers {{{ + + onSync: -> + return unless @ready + console.info "#this.sync() --> success!" + # TODO: UI alert + @chartOptions @model.getOptions(), {+silent} + @render() + + onStartWaiting: -> + status = @checkWaiting() + # console.log "#this.onStartWaiting!", status + + onStopWaiting: -> + status = @checkWaiting() + # console.log "#this.onStopWaiting!", status + + onModelError: -> + console.error "#this.error!", arguments + # TODO: UI alert + + onModelChange: -> + changes = @model.changedAttributes() + options = @model.getOptions() + # console.log """ + # Graph.changed( options ) -> + # \tchanges: #{JSON.stringify changes} + # \toptions: #{JSON.stringify options} + # \t^opts: #{JSON.stringify _.intersection _.keys(changes), _.keys(options)} + # """ + # @chart?.updateOptions file:that if changes?.dataset + @chartOptions options, {+silent} if changes?.options + + # Needed because (sigh) _.debounce returns undefined, and we need to preventDefault() + stopAndRender : -> @render ... ; false + + + ### }}} + ### Spinner {{{ + + /** + * Retrieve or construct the spinner. + */ + spinner: -> + el = @$ '.graph-spinner' + unless el.data 'spinner' + ### Spin.js Options ### + opts = + lines : 9 # [12] The number of lines to draw + length : 2 # [7] The length of each line + width : 1 # [5] The line thickness + radius : 7 # [10] The radius of the inner circle + rotate : -10.5 # [0] rotation offset + trail : 50 # [100] Afterglow percentage + opacity : 1/4 # [1/4] Opacity of the lines + shadow : false # [false] Whether to render a shadow + speed : 1 # [1] Spins per second + zIndex : 2e9 # [2e9] zIndex; uses a very high z-index by default + color : '#000' # ['#000'] Line color; '#rgb' or '#rrggbb'. + top : 'auto' # ['auto'] Top position relative to parent in px; 'auto' = center vertically. + left : 'auto' # ['auto'] Left position relative to parent in px; 'auto' = center horizontally. + className : 'spinner' # ['spinner'] CSS class to assign to the element + fps : 20 # [20] Frames per second when falling back to `setTimeout()`. + hwaccel : Modernizr.csstransforms3d # [false] Whether to use hardware acceleration. + + isHidden = el.css('display') is 'none' + el.show().spin opts + el.hide() if isHidden + el + + checkWaiting: -> + spinner = @spinner() + if isWaiting = (@waitingOn > 0) + spinner.show() + if spinner.find('.spinner').css('top') is '0px' + # delete spinner + spinner.spin(false) + # re-add to DOM with correct parent sizing + @spinner() + else + spinner.hide() + isWaiting + + # }}} + + diff --git a/lib/graph/index.co b/lib/graph/index.co index 8ea477e..18203dd 100644 --- a/lib/graph/index.co +++ b/lib/graph/index.co @@ -1,6 +1,7 @@ models = require 'kraken/graph/graph-model' +base_views = require 'kraken/graph/graph-view' display_views = require 'kraken/graph/graph-display-view' edit_views = require 'kraken/graph/graph-edit-view' index_views = require 'kraken/graph/graph-list-view' -exports import models import display_views import edit_views import index_views +exports import models import base_views import display_views import edit_views import index_views diff --git a/lib/template/graph-edit.jade b/lib/template/graph-edit.jade index 83179c1..6c39824 100644 --- a/lib/template/graph-edit.jade +++ b/lib/template/graph-edit.jade @@ -6,7 +6,7 @@ section.graph-edit.graph(id=graph_id) .graph-viewport-row.row-fluid .viewport - .graph-label + .graph-legend .graph-settings-row.row-fluid .graph-settings.tabbable diff --git a/www/css/graph.styl b/www/css/graph.styl index 64df61d..7a9ee67 100644 --- a/www/css/graph.styl +++ b/www/css/graph.styl @@ -26,7 +26,7 @@ section.graph min-height 320px overflow hidden - .graph-label + .graph-legend position absolute z-index 100 top 1em @@ -51,7 +51,7 @@ section.graph .suffix text-align left - .viewport:hover + .graph-label + .viewport:hover + .graph-legend border 1px solid $light /* }}} */ diff --git a/www/modules.yaml b/www/modules.yaml index c76da7b..9e51454 100644 --- a/www/modules.yaml +++ b/www/modules.yaml @@ -111,6 +111,7 @@ dev: - index - graph: - graph-model + - graph-view - graph-display-view - graph-edit-view - graph-list-view -- 1.7.0.4