removeAllSubviews: ->
@subviews.forEach @removeSubview, this
- @subviews = new ViewList
+ # @subviews = new ViewList
this
/* * * * Events * * * */
- bubbleEvent: (evt) ->
+ bubbleEventDown: (evt) ->
@invokeSubviews 'trigger', ...arguments
this
- redispatch: (evt) ->
- @trigger ...arguments
+ redispatch: (evt, ...args) ->
+ @trigger evt, this, ...args
this
onlyOnReturn: (fn, ...args) ->
json = ChartOptionView.__super__.toTemplateLocals ...
json.id or= _.camelize json.name
json.value ?= ''
- json.value = JSON.stringify v if v = json.value and (_.isArray(v) or _.isPlainObject(v))
+ v = json.value
+ json.value = JSON.stringify(v) if v and ( _.isArray(v) or _.isPlainObject(v) )
json
/**
* @type String
* @readonly
*/
- CHART_SPEC_URL : null
+ SPEC_URL : null
/**
* Chart-type name.
return this if @ready
proto = @constructor::
jQuery.ajax do
- url : @CHART_SPEC_URL
+ url : @SPEC_URL
success : (spec) ~>
+ proto.spec = spec
proto.options_ordered = spec
proto.options = _.synthesize spec, -> [it.name, it]
proto.ready = true
data = @getData()
options = @getDefaultOptions() import @transform @model, @view
viewport = @getElementsForRole 'viewport'
+ return @lastChart unless data?.length and viewport?.length
@lastChart = @renderChart data, viewport, options, @chart
class exports.DygraphsChartType extends ChartType
__bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]>
- CHART_SPEC_URL : '/schema/dygraph.json'
+ SPEC_URL : '/schema/dygraph.json'
+
+ # NOTE: ChartType.register() must come AFTER `typeName` declaration.
typeName : 'dygraphs'
ChartType.register this
{ width, height }
+ /**
+ * Resizes the HTML viewport.
+ */
resizeViewport: ->
size = @determineSize()
@getElementsForRole 'viewport' .css size
@model.once 'ready', @onReady, this
onReady: ->
- console.log "#this.onReady! #{@model.metrics}"
+ # console.log "#this.onReady! #{@model.metrics}"
dataset = @model
@model.metrics.each @addMetric, this
@dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources}
{ @graph_id, @datasources } import attrs
addMetric: (metric) ->
- console.log "#this.addMetric!", metric
+ # console.log "#this.addMetric!", metric
return metric if @metric_views.findByModel metric
view = new MetricEditView {model:metric, @graph_id, dataset:@model, @datasources}
.on 'metric-update', @onUpdateMetric, this
metric
removeMetric: (metric) ->
- console.log "#this.removeMetric!", metric
+ # console.log "#this.removeMetric!", metric
return unless view = @metric_views.findByModel metric
@metric_views.remove view
@removeSubview view
@$el.css 'min-height', newMinHeight
onUpdateMetric: ->
- console.log "#this.onUpdateMetric!"
+ # console.log "#this.onUpdateMetric!"
@trigger 'metric-change', @model, this
@render()
BaseModel::initialize ...
@set 'metrics', @metrics, {+silent}
@on 'change:metrics', @onMetricChange, this
+ # @metrics.on 'add remove reset', ~>
+ # @trigger 'change:metrics', @metrics, this
load: (opts={}) ->
unless @metrics.length
return @triggerReady()
- console.log "#this.load()..."
+ # console.log "#this.load()..."
@wait()
@loading = true
@trigger 'load', this
Seq @metrics.models
.parEach_ (next, metric) ->
metric.once 'ready', next.ok .load()
- # .parEach_ (next, metric) ->
- # metric.on 'load-data-success', next.ok .loadData()
.seq ~>
- console.log "#{this}.load() complete!"
+ # console.log "#{this}.load() complete!"
@loading = false
@unwait() # terminates the `load` wait
@triggerReady()
*/
getData: ->
return [] unless @ready
- _.zip ...@getColumns()
+ columns = @getColumns()
+ if columns?.length
+ _.zip ...columns
+ else
+ []
/**
* @returns {Array<Array>} List of all columns (including date column).
newMetric: ->
index = @metrics.length
@metrics.add m = new Metric { index, color:ColorBrewer.Spectral[11][index] }
- # @get 'metrics' .push m.attributes
+ m.on 'ready', ~> @trigger 'metric-data-loaded', this, m
# @trigger 'change:metrics', this, @metrics, 'metrics'
# @trigger 'change', this, @metrics, 'metrics'
m
onMetricChange: ->
- console.log "#this.onMetricChange! ready=#{@ready}"
+ # console.log "#this.onMetricChange! ready=#{@ready}"
@resetReady()
@load()
template : require 'kraken/template/dataset'
events:
- 'click .new-metric-button' : 'newMetric'
- 'click .metrics .dataset-metric' : 'selectMetric'
+ 'click .new-metric-button' : 'onNewMetric'
+ 'click .delete-metric-button' : 'onDeleteMetric'
+ 'click .metrics .dataset-metric' : 'selectMetric'
views_by_cid : {}
active_view : null
@model.metrics
.on 'add', @addMetric, this
.on 'remove', @removeMetric, this
+ .on 'change', @onMetricChange, this
.on 'reset', @addAllMetrics, this
- newMetric: ->
- console.log "#this.newMetric!"
- # triggers 'add' on @model.metrics
- @model.newMetric()
- false
-
addMetric: (metric) ->
- console.log "#this.addMetric!", metric
+ # console.log "#this.addMetric!", metric
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
@trigger 'add-metric', metric, view, this
@render()
view
removeMetric: (metric) ->
- console.log "#this.removeMetric!", metric
+ if metric instanceof [jQuery.Event, Event]
+ metric = @getMetricForElement metric.target
+ # console.log "#this.removeMetric!", metric
+ return unless metric
if view = @views_by_cid[metric.cid]
@removeSubview view
delete @views_by_cid[metric.cid]
view
addAllMetrics: ->
- console.log "#this.addAllMetrics! --> #{@model.metrics}"
+ # console.log "#this.addAllMetrics! --> #{@model.metrics}"
@removeAllSubviews()
@model.metrics.each @addMetric, this
this
+
selectMetric: (metric) ->
if metric instanceof [jQuery.Event, Event]
- metric = $ metric.currentTarget .data 'model'
- view = @active_view = @views_by_cid[metric.cid]
+ metric = @getMetricForElement metric.target
# console.log "#this.selectMetric!", metric
+ return unless metric
+ view = @active_view = @views_by_cid[metric.cid]
@$ '.metrics .dataset-metric' .removeClass 'metric-active'
view.$el.addClass 'metric-active'
- view.$el.find '.activity-arrow' .css 'font-size', 2+view.$el.height()
+ view.$ '.activity-arrow' .css 'font-size', 2+view.$el.height()
@trigger 'select-metric', metric, view, this
this
+ onMetricChange: (metric) ->
+ return unless view = @views_by_cid[metric?.cid]
+ view.$ '.activity-arrow:visible' .css 'font-size', 2+view.$el.height()
+
+ onNewMetric: ->
+ # console.log "#this.newMetric!"
+ # triggers 'add' on @model.metrics
+ @model.newMetric()
+ false
+
+ onDeleteMetric: (evt) ->
+ metric = @getMetricForElement evt.target
+ # console.log "#this.onDeleteMetric!", metric
+ # Triggers a 'remove' event, which in turn calls `removeMetric()`
+ @model.metrics.remove metric
+ false
+
+
+ getMetricForElement: (el) ->
+ $ el .parents '.dataset-metric' .eq(0).data 'model'
# }}}
console.log "#this.onLoadDataSuccess #{@data}"
@unwait()
@trigger 'load-data-success', this
+ @triggerReady()
onLoadDataError: (jqXHR, txtStatus, err) ->
console.error "#this Error loading data! -- #msg: #{err or ''}"
toTemplateLocals: ->
locals = @model.toJSON()
- locals import {@graph_id, @dataset, @datasources}
+ locals import {@graph_id, @dataset, @datasources, cid:@model.cid}
ds = @model.source
hasSource = @model.get('source_id')? and ds
this
onAttach: ->
- console.log "#this.onAttach!"
+ # console.log "#this.onAttach!"
@$ '.color-swatch'
.data 'color', @model.get 'color'
.colorpicker()
* attempt to graph unconfigured crap.
*/
isOk: ->
- @source # and _.every @get('timespan'), op.ok
+ @source?.ready # and _.every @get('timespan'), op.ok
# }}}
* - Chart options, using ChartOptionScaffold
*/
GraphEditView = exports.GraphEditView = GraphView.extend do # {{{
- __bind__ : <[ wait unwait onScaffoldChange onFirstClickRenderOptionsTab onFirstClickRenderDataTab ]>
+ __bind__ : <[
+ wait unwait onChartTypeReady onScaffoldChange
+ onFirstClickRenderOptionsTab onFirstClickRenderDataTab
+ ]>
className : 'graph-edit graph'
template : require 'kraken/template/graph-edit'
### Chart Options Tab, Scaffold
@scaffold = @addSubview new ChartOptionScaffold
- @scaffold.collection.reset that if o.graph_spec
- @scaffold.on 'change', @onScaffoldChange
- @chartOptions @model.getOptions(), {+silent}
+ # @scaffold.collection.reset that if o.graph_spec
+ # @scaffold.on 'change', @onScaffoldChange
+ @chartType.on 'ready', @onChartTypeReady
### Graph Data UI
@$el.on 'click', '.graph-data-tab', @onFirstClickRenderDataTab
@$el.on 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab
-
# Kick off model load chain
@loadData()
+ onChartTypeReady: ->
+ @scaffold.collection.reset @chartType.options_ordered
+ @scaffold.on 'change', @onScaffoldChange
+ @chartOptions @model.getOptions(), {+silent}
+
onReady: ->
return if @ready
console.log "(#this via GraphEditView).ready!"
@chartOptions @model.getOptions(), {+silent}
@render()
+ @model.dataset.metrics
+ .on 'add remove change', @render, this
+ @model
+ .on 'metric-data-loaded', @render, this
+
# fix up the spinner element once the DOM is settled
_.delay @checkWaiting, 50
initialize : ->
@model = @collection or= new GraphList
BaseView::initialize ...
- console.log "#this.initialize!"
+ # console.log "#this.initialize!"
toTemplateLocals: ->
locals = BaseView::toTemplateLocals ...
# Insert submodels in place of JSON
@dataset = new DataSet {id:@id, ...@get 'data'}
- # .on 'change', @onDataSetChange, this
+ .on 'change', @onDataSetChange, this
+ .on 'metric-data-loaded', (dataset, metric) ~>
+ @trigger 'metric-data-loaded', this, metric
@set 'data', @dataset, {+silent}
@trigger 'init', this
# Fetch model if
.seq_ (next) ~>
return next.ok() if @isNew()
- console.log "#{this}.fetch()..."
+ # console.log "#{this}.fetch()..."
@wait()
@fetch do
error : @unwaitAnd (err) ~>
# Done!
.seq ~>
- console.log "#{this}.load() complete!"
+ # console.log "#{this}.load() complete!"
@loading = false
@unwait() # terminates the `load` wait
@triggerReady()
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()
+ metric.source.once 'load-data-success', next.ok .loadData()
.seq ~>
console.log "#{this}.loadData() complete!"
@loading = false
onDataSetChange: ->
console.log "#this.onDataSetChange!"
- @set 'data', @dataset, {+silent}
+ # @set 'data', @dataset, {+silent}
+ # @trigger 'change:data', this, @dataset, 'data'
+ @trigger 'change', this, @dataset, 'data'
### Accessors
@trigger "change:options.#key", this, value, key, opts unless opts.silent
if changed and not opts.silent
- @trigger "change:options", this, options, 'options', opts
+ @trigger "change:options", this, options, 'options', opts
+ @trigger "change", this, options, 'options', opts
this
unsetOption: (key, opts={}) ->
unless @optionCascade.unset(key) is void or opts.silent
+ options = @get 'options'
@trigger "change:options.#key", this, void, key, opts
- @trigger "change:options", this, @get('options'), 'options', opts
+ @trigger "change:options", this, options, 'options', opts
+ @trigger "change", this, options, 'options', opts
this
inheritOption: (key, opts={}) ->
old = @getOption(key)
@optionCascade.inherit(key)
unless @getOption(key) is old or opts.silent
+ options = @get 'options'
@trigger "change:options.#key", this, void, key, opts
- @trigger "change:options", this, @get('options'), 'options', opts
+ @trigger "change:options", this, options, 'options', opts
+ @trigger "change:options", this, options, 'options', opts
this
getOptions: (opts={}) ->
# Bind to URL changes
History.Adapter.bind window, 'statechange', ->
- console.log 'StateChange!\n\n', String(root.location), '\n\n'
+ # console.log 'StateChange!\n\n', String(root.location), '\n\n'
next.ok()
error : (err) -> console.error err
.seq ->
- console.log 'All data loaded!'
+ # console.log 'All data loaded!'
jQuery main
# Bind to URL changes
History.Adapter.bind window, 'statechange', ->
- console.log 'StateChange!\n\n', String(root.location), '\n\n'
+ # console.log 'StateChange!\n\n', String(root.location), '\n\n'
current = @model.getValue()
return if _.isEqual val, current
- console.log "#this.onChange( #current -> #val )"
+ # console.log "#this.onChange( #current -> #val )"
@model.setValue val, {+silent}
@trigger 'change', this
td.col-source(data-bind="source") #{source}
td.col-time(data-bind="timespan", data-bind-escape="false") #{timespan}
td.col-actions
- a.delete-metric-button.close(href="#") ×
+ a.delete-metric-button.control.close(href="#") ×
.activity-arrow: div.inner
th.col-times Timespan
th.col-actions Actions
tbody.metrics(data-subview="DataSetMetricView")
- //- DataSetMetricViews attach here
-section.datasource-ui(class="datasource-ui-#{source_id}")
+section.datasource-ui(class="datasource-ui-#{cid}")
- section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui.datasource-ui-#{source_id} .datasource-selector")
+ section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui.datasource-ui-#{cid} .datasource-selector")
i.expand-datasource-ui-button.icon-chevron-down
i.collapse-datasource-ui-button.icon-chevron-up
- var ds = source.attributes
+- var id = ds.id || source.cid
- var activeClass = (source_id === ds.id ? 'active' : '')
-.datasource-source.tab-pane(class="datasource-source-#{ds.id} #{activeClass}")
+.datasource-source.tab-pane(class="datasource-source-#{id} #{activeClass}")
.datasource-source-details.well
.source-name #{ds.name}
.source-id #{ds.id}
if Cls.__superclass__
superclass = that
else
- Cls = Cls.constructor unless typeof Cls is 'function'
- superclass = Cls.__super__?.constructor
+ Cls = Cls.constructor if typeof Cls is not 'function'
+ if Cls.__super__?.constructor
+ superclass = that
+ else if Cls::constructor is not Cls
+ superclass
if superclass
[superclass].concat getSuperClasses superclass
--- /dev/null
+moment = require 'moment'
+
+{ _, op,
+} = require 'kraken/util'
+
+
+_fmt = do
+
+ /**
+ * Formats a date for display on an axis: `MM/YYYY`
+ * @param {Date} d Date to format.
+ * @returns {String}
+ */
+ axisDateFormatter: (d) ->
+ moment(d).format 'MM/YYYY'
+
+ /**
+ * Formats a date for display in the legend: `DD MMM YYYY`
+ * @param {Date} d Date to format.
+ * @returns {String}
+ */
+ dateFormatter: (d) ->
+ moment(d).format 'DD MMM YYYY'
+
+ /**
+ * Formats a number for display, first dividing by the greatest suffix
+ * of {B = Billions, M = Millions, K = Thousands} that results in a
+ * absolute value greater than 0, and then rounding to `digits` using
+ * `result.toFixed(digits)`.
+ *
+ * @param {Number} n Number to format.
+ * @param {Number} [digits=2] Number of digits after the decimal to always display.
+ * @returns {String} Formatted number.
+ */
+ _numberFormatter: (n, digits=2) ->
+ for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]]
+ break if isNaN d
+ 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, digits=2) ->
+ { whole, fraction, suffix } = _fmt._numberFormatter n, digits
+ "#whole#fraction#suffix"
+
+ numberFormatterHTML: (n, digits=2) ->
+ { whole, fraction, suffix } = _fmt._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>"
+
+
+
+
+exports = _fmt
*/
remove: (obj, v) ->
values = [].slice.call arguments, 1
- if _.isArray obj
+ if _.isArray(obj) or obj instanceof Array
for v of values
idx = obj.indexOf v
obj.splice idx, 1 if idx is not -1