{
+ "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,
"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",
- "dataset": "/data/datasources/rc/rc_page_requests.csv",
- "data": {
- "metrics": [{
- "source_id": "rc_page_requests",
- "source_col": 1,
- "label": "All Wikipedias (+Mobile)",
- "color": "#E62F74"
- }, {
- "source_id": "rc_page_requests",
- "source_col": 2,
- "label": "English",
- "color": "#244792"
- }]
}
}
* Subsequent listeners added on this event will be auto-triggered.
* @returns {this}
*/
- triggerReady: ->
- return this if @ready
- @ready = true
- @trigger 'ready', this
+ triggerReady: (lock='ready', event='ready') ->
+ return this if @[lock]
+ @[lock] = true
+ @trigger event, this
this
/**
* 'ready-reset' event.
* @returns {this}
*/
- resetReady: ->
- return this unless @ready
- @ready = false
- @trigger 'ready-reset', this
+ resetReady: (lock='ready', event='ready') ->
+ return this unless @[lock]
+ @[lock] = false
+ @trigger "#event-reset", this
this
/**
* @param {Object} [context]
* @returns {this}
*/
- on: (events, callback, context) ->
+ on: (events, callback, context=this) ->
return this if not callback
Backbone.Events.on ...
if @ready and _.contains events.split(/\s+/), 'ready'
- callback.call context or this, this
+ callback.call context, this
this
+ makeHandlersForCallback: (cb) ->
+ success : ~> cb.call this, [null].concat arguments
+ error : ~> cb.call this, it
+
+
### Synchronization
self.unwait(); fn ...
+
+ ###
+
getClassName: ->
"#{@..name or @..displayName}"
@__superclass__ = @..__super__.constructor
@waitingOn = 0
Backbone.Model ...
- @trigger 'create', this
+ # @..trigger 'create', this
#
+ ### Data & Model Loading
+
+ /**
+ * Override to customize what data or assets the model requires,
+ * and how they should be loaded.
+ *
+ * By default, `load()` simply calls `loadModel()` via `loader()`.
+ *
+ * @see BaseModel#loader
+ * @see BaseModel#loadModel
+ * @returns {this}
+ */
+ load: ->
+ console.log "#this.load()"
+ @loader do
+ start : @loadModel
+ completeEvent : 'fetch-success'
+ this
+
+ /**
+ * Wraps the loading workflow boilerplate:
+ * - Squelches multiple loads from running at once
+ * - Squelches loads post-ready, unless forced
+ * - Triggers a start event
+ * - Triggers "ready" when complete
+ * - Wraps workflow with wait/unwait
+ * - Cleans up "loading" state
+ *
+ * @protected
+ * @param {Object} [opts={}] Options:
+ * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context.
+ * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load.
+ * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed.
+ * @param {Boolean} [opts.force=false] If true, move forward with the load even if we're ready.
+ * @returns {this}
+ */
+ loader: (opts={}) ->
+ opts = { -force, startEvent:'load', completeEvent:'load-success', ...opts }
+ @resetReady() if opts.force
+ return this if not opts.start or @loading or @ready
+
+ @wait()
+ @loading = true
+ @trigger opts.startEvent, this
+
+ # Register a handler for the post-load event that will run only once
+ @once opts.completeEvent, ~>
+ console.log "#{this}.onLoadComplete()"
+ @loading = false
+ @unwait() # terminates the `load` wait
+ @trigger 'load-success', this unless opts.completeEvent is 'load-success'
+ @triggerReady()
+
+ # Finally, start the loading process
+ opts.start.call this
+ this
+
+ /**
+ * Runs `.fetch()`, triggering a `fetch` event at start, and
+ * `fetch-success` / `fetch-error` on completion.
+ *
+ * @protected
+ * @returns {this}
+ */
+ loadModel: ->
+ @wait()
+ @trigger 'fetch', this
+ @fetch do
+ success : ~> @unwait(); @trigger 'fetch-success', this
+ error : ~> @unwait(); @trigger 'fetch-error', this, ...arguments
+ this
### Serialization
toString: ->
"#{@getClassName()}[#{@length}]"
- # toString: ->
- # modelIds = @models
- # .map -> "\"#{it.id ? it.cid}\""
- # .join ', '
- # "#{@getClassName()}[#{@length}](#modelIds)"
+ toStringWithIds: ->
+ modelIds = @models
+ .map -> "\"#{it.id ? it.cid}\""
+ .join ', '
+ "#{@getClassName()}[#{@length}](#modelIds)"
# }}}
.parMap_ (next, id) ~>
return next.ok(that) if @cache.get id
@register @createModel id
- .on 'ready', next
+ .on 'ready', -> next.ok it
.load()
.unflatten()
.seq (models) ->
} = require 'kraken/dataset/dataset-view'
{ MetricEditView,
} = require 'kraken/dataset/metric-edit-view'
-
+{ DataSource,
+} = require 'kraken/dataset/datasource-model'
/**
- * @class
+ * @class DataSet selection and customization UI (root of the `data` tab).
*/
DataView = exports.DataView = BaseView.extend do # {{{
- __bind__ : <[ ]>
+ __bind__ : <[ onMetricsChanged ]>
tagName : 'section'
className : 'data-ui'
template : require 'kraken/template/data'
datasources : null
-
+ /**
+ * @constructor
+ */
constructor: function DataView
BaseView ...
@graph_id = @options.graph_id
BaseView::initialize ...
@metric_views = new ViewList
- @datasources = @model.sources
+ @datasources = DataSource.getAllSources()
@model.metrics
- .on 'add', @addMetric, this
- .on 'remove', @removeMetric, this
- @on 'ready', @onReady, this
- @load()
+ .on 'add', @addMetric, this
+ .on 'remove', @removeMetric, this
+ @model.once 'ready', @onReady, this
onReady: ->
+ console.log "#this.onReady! #{@model.metrics}"
dataset = @model
@model.metrics.each @addMetric, this
- # @metric_edit_view = @addSubview new MetricEditView {@graph_id, dataset, @datasources}
- # @metric_edit_view
- # .on 'update', @onUpdateMetric, this
-
@dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources}
@addSubview @dataset_view
.on 'add-metric', @onMetricsChanged, this
.on 'edit-metric', @editMetric, this
@render()
+ @triggerReady()
this
-
- load: ->
- @wait()
- # $.getJSON '/datasources/all', (@data) ~>
- # _.each @data, @canonicalizeDataSource, this
- # @model.sources.reset _.map @data, -> it
- @unwait()
- # @render()
- @triggerReady()
-
/**
* Transform the `columns` field to ensure an Array of {label, type} objects.
*/
attrs = _.clone @model.attributes
{ @graph_id, @datasources } import attrs
- # Don't rebuild HTML, simply notify subviews
- # render: ->
- # @renderSubviews()
- # @trigger 'render', this
- # this
-
addMetric: (metric) ->
console.log "#this.addMetric!", metric
return metric if @metric_views.findByModel metric
- view = new MetricEditView {model:metric, @graph_id, dataset, @datasources}
+ view = new MetricEditView {model:metric, @graph_id, dataset:@model, @datasources}
+ view.on 'update', @onUpdateMetric, this
@metric_views.push @addSubview view
metric
@metric_views.invoke 'hide'
@metric_edit_view = @metric_views.findByModel metric
@metric_edit_view?.show()
- @onMetricsChanged()
+ _.delay @onMetricsChanged, 10
onMetricsChanged: ->
oldMinHeight = parseInt @$el.css 'min-height'
newMinHeight = Math.max do
- oldMinHeight
@dataset_view.$el.height()
@metric_edit_view?.$el.height()
# console.log 'onMetricsChanged!', oldMinHeight, '-->', newMinHeight
*/
DataSet = exports.DataSet = BaseModel.extend do # {{{
urlRoot : '/datasets'
- ready : false
/**
* @type DataSourceList
initialize : ->
BaseModel::initialize ...
- @sources = new DataSourceList
@metrics = new MetricList @attributes.metrics
@on 'change:metrics', @onMetricChange, this
load: (opts={}) ->
@resetReady() if opts.force
- return this if @ready
+ return this if @loading or @ready
+
+ unless @metrics.length
+ return @triggerReady()
+
+ console.log "#this.load()..."
@wait()
+ @loading = true
@trigger 'load', this
- Seq _.unique @metrics.pluck 'source_id'
- .parMap_ (next, source_id) ->
- DataSource.lookup source_id, next
- .seqEach_ (next, source) ~>
- @sources.add source
- next.ok source
+ 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!"
+ @loading = false
@unwait() # terminates the `load` wait
@triggerReady()
this
m
onMetricChange: ->
- @metrics.reset @get 'metrics'
+ @resetReady()
+ # @metrics.reset @get 'metrics'
+ @load()
# TODO: toJSON() must ensure columns in MetricList are ordered by index
# ...in theory, MetricList.comparator now does this
{@graph_id, @datasources, @dataset} = @options
BaseView::initialize ...
@views_by_cid = {}
- @addAllMetrics()
+ @model
+ .on 'ready', @addAllMetrics, this
@model.metrics
.on 'add', @addMetric, this
.on 'remove', @removeMetric, this
metric.view
addAllMetrics: ->
+ console.log "#this.addAllMetrics! --> #{@model.metrics}"
@removeAllSubviews()
@model.metrics.each @addMetric, this
this
} = require 'kraken/util'
{ TimeSeriesData, CSVData,
} = require 'kraken/timeseries'
-{ BaseModel, BaseList, BaseView,
+{ BaseModel, BaseList, ModelCache,
} = require 'kraken/base'
{ Metric, MetricList,
} = require 'kraken/dataset/metric-model'
* @class
*/
DataSource = exports.DataSource = BaseModel.extend do # {{{
- __bind__ : <[ onLoadSuccess onLoadError ]>
+ __bind__ : <[ onLoadDataSuccess onLoadDataError ]>
urlRoot : '/datasources'
ready : false
@constructor.register this
@metrics = new MetricList @attributes.metrics
@on 'change:metrics', @onMetricChange, this
- # @load()
canonicalize: (ds) ->
{idx, label, type:cols.types[idx] or 'int'}
ds
- load: ->
- @trigger 'load', this
- url = @get 'url'
+
+
+ loadAll: ->
+ @loader start: ->
+ Seq()
+ .seq_ (next) ~>
+ @once 'fetch-success', next.ok
+ @loadModel()
+ .seq_ (next) ~>
+ @once 'load-data-success', next.ok
+ @loadData()
+ .seq ~>
+ @trigger 'load-success', this
+ this