this
render: ->
+ @wait()
if @isBuilt
@update()
else
@build()
@renderSubviews()
@trigger 'render', this
+ @unwait()
this
renderSubviews: ->
{ _, 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'
'click .export-button' : 'exportChart'
# 'click .load-button' : 'load'
- data : {}
- ready : false
constructor: function GraphDisplayView
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
- """
- <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
-
/**
* Selects the graph permalink input field.
*/
# defer, the focusing click will
# unselect the text.
_.defer( ~> @$ '.graph-permalink input' .select() )
-
-
-
- # Needed because (sigh) _.debounce returns undefined
- stopAndRender: ->
- @render ...
- false
-
# }}}
# }}}
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'
* - 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'
'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
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
.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
### 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'
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
### }}}
- ### 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
- "<span class='value'>
- <span class='whole'>#whole</span>
- <span class='fraction'>#fraction</span>
- <span class='suffix'>#suffix</span>
- </span>"
-
- ### }}}
### 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
@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
-
# }}}
# }}}
--- /dev/null
+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
+ "<span class='value'>
+ <span class='whole'>#whole</span>
+ <span class='fraction'>#fraction</span>
+ <span class='suffix'>#suffix</span>
+ </span>"
+
+ ### }}}
+ ### 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
+
+ # }}}
+
+
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
.graph-viewport-row.row-fluid
.viewport
- .graph-label
+ .graph-legend
.graph-settings-row.row-fluid
.graph-settings.tabbable
min-height 320px
overflow hidden
- .graph-label
+ .graph-legend
position absolute
z-index 100
top 1em
.suffix
text-align left
- .viewport:hover + .graph-label
+ .viewport:hover + .graph-legend
border 1px solid $light
/* }}} */
- index
- graph:
- graph-model
+ - graph-view
- graph-display-view
- graph-edit-view
- graph-list-view