From f7c5fded132151e468aab53451a4f7753a665e0b Mon Sep 17 00:00:00 2001 From: dsc Date: Mon, 30 Apr 2012 11:59:23 -0700 Subject: [PATCH] Checkpoint: Events mostly flow upward. --- data/graphs/ohai.json | 51 +++++++++-------- lib/base/base-mixin.co | 28 ++++++--- lib/base/base-model.co | 83 +++++++++++++++++++++++++-- lib/base/model-cache.co | 2 +- lib/dataset/data-view.co | 48 +++++---------- lib/dataset/dataset-model.co | 27 ++++++--- lib/dataset/dataset-view.co | 4 +- lib/dataset/datasource-model.co | 114 +++++++++++++++--------------------- lib/dataset/datasource-ui-view.co | 15 ++++- lib/dataset/metric-edit-view.co | 24 ++++---- lib/dataset/metric-model.co | 48 ++++++++-------- lib/graph/graph-edit-view.co | 80 +++++++++++++++----------- lib/graph/graph-model.co | 96 +++++++++++++++++++++---------- lib/main-edit.co | 7 -- lib/template/datasource-ui.jade | 6 +- lib/util/backbone.co | 16 +++++- lib/util/event/ready-emitter.co | 1 + lib/util/event/waiting-emitter.co | 2 + 18 files changed, 387 insertions(+), 265 deletions(-) diff --git a/data/graphs/ohai.json b/data/graphs/ohai.json index 224d8d1..c15f14c 100644 --- a/data/graphs/ohai.json +++ b/data/graphs/ohai.json @@ -1,4 +1,32 @@ { + "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, @@ -86,28 +114,5 @@ "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" - }] } } diff --git a/lib/base/base-mixin.co b/lib/base/base-mixin.co index 1dc1a2b..0b081a7 100644 --- a/lib/base/base-mixin.co +++ b/lib/base/base-mixin.co @@ -44,10 +44,10 @@ BaseBackboneMixin = exports.BaseBackboneMixin = * 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 /** @@ -55,10 +55,10 @@ BaseBackboneMixin = exports.BaseBackboneMixin = * '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 /** @@ -71,13 +71,18 @@ BaseBackboneMixin = exports.BaseBackboneMixin = * @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 @@ -127,6 +132,9 @@ BaseBackboneMixin = exports.BaseBackboneMixin = self.unwait(); fn ... + + ### + getClassName: -> "#{@..name or @..displayName}" diff --git a/lib/base/base-model.co b/lib/base/base-model.co index 499fb7d..348ea82 100644 --- a/lib/base/base-model.co +++ b/lib/base/base-model.co @@ -18,7 +18,7 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ @__superclass__ = @..__super__.constructor @waitingOn = 0 Backbone.Model ... - @trigger 'create', this + # @..trigger 'create', this @@ -52,7 +52,78 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ # + ### 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 @@ -149,11 +220,11 @@ BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{ 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)" # }}} diff --git a/lib/base/model-cache.co b/lib/base/model-cache.co index f3ef08c..f636a7d 100644 --- a/lib/base/model-cache.co +++ b/lib/base/model-cache.co @@ -146,7 +146,7 @@ class exports.ModelCache extends ReadyEmitter .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) -> diff --git a/lib/dataset/data-view.co b/lib/dataset/data-view.co index 9fc9f3d..6c8d115 100644 --- a/lib/dataset/data-view.co +++ b/lib/dataset/data-view.co @@ -7,13 +7,14 @@ Seq = require 'seq' } = 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' @@ -21,7 +22,9 @@ DataView = exports.DataView = BaseView.extend do # {{{ datasources : null - + /** + * @constructor + */ constructor: function DataView BaseView ... @@ -29,20 +32,16 @@ DataView = exports.DataView = BaseView.extend do # {{{ @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 @@ -50,19 +49,10 @@ DataView = exports.DataView = BaseView.extend do # {{{ .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. */ @@ -89,16 +79,11 @@ DataView = exports.DataView = BaseView.extend do # {{{ 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 @@ -114,12 +99,11 @@ DataView = exports.DataView = BaseView.extend do # {{{ @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 diff --git a/lib/dataset/dataset-model.co b/lib/dataset/dataset-model.co index e0722b4..bec6098 100644 --- a/lib/dataset/dataset-model.co +++ b/lib/dataset/dataset-model.co @@ -17,7 +17,6 @@ ColorBrewer = require 'colorbrewer' */ DataSet = exports.DataSet = BaseModel.extend do # {{{ urlRoot : '/datasets' - ready : false /** * @type DataSourceList @@ -40,7 +39,6 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ initialize : -> BaseModel::initialize ... - @sources = new DataSourceList @metrics = new MetricList @attributes.metrics @on 'change:metrics', @onMetricChange, this @@ -48,16 +46,23 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ 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 @@ -106,7 +111,9 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ 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 diff --git a/lib/dataset/dataset-view.co b/lib/dataset/dataset-view.co index b201229..ecdf08b 100644 --- a/lib/dataset/dataset-view.co +++ b/lib/dataset/dataset-view.co @@ -27,7 +27,8 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{ {@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 @@ -63,6 +64,7 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{ metric.view addAllMetrics: -> + console.log "#this.addAllMetrics! --> #{@model.metrics}" @removeAllSubviews() @model.metrics.each @addMetric, this this diff --git a/lib/dataset/datasource-model.co b/lib/dataset/datasource-model.co index 7b824eb..75bf5e0 100644 --- a/lib/dataset/datasource-model.co +++ b/lib/dataset/datasource-model.co @@ -2,7 +2,7 @@ } = require 'kraken/util' { TimeSeriesData, CSVData, } = require 'kraken/timeseries' -{ BaseModel, BaseList, BaseView, +{ BaseModel, BaseList, ModelCache, } = require 'kraken/base' { Metric, MetricList, } = require 'kraken/dataset/metric-model' @@ -12,7 +12,7 @@ * @class */ DataSource = exports.DataSource = BaseModel.extend do # {{{ - __bind__ : <[ onLoadSuccess onLoadError ]> + __bind__ : <[ onLoadDataSuccess onLoadDataError ]> urlRoot : '/datasources' ready : false @@ -61,7 +61,6 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{ @constructor.register this @metrics = new MetricList @attributes.metrics @on 'change:metrics', @onMetricChange, this - # @load() canonicalize: (ds) -> @@ -83,44 +82,59 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{ {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 + + loadData: -> + @wait() + @trigger 'load-data', this + return @onLoadDataSuccess @data if @data switch @get 'format' - case 'json' - @loadJSON url - case 'csv' - @loadCSV url + case 'json' then @loadJSON() + case 'csv' then @loadCSV() default console.error "#this.load() Unknown Data Format!" - @onLoadError null, 'Unknown Data Format!', new Error 'Unknown Data Format!' + @onLoadDataError null, 'Unknown Data Format!', new Error 'Unknown Data Format!' this - loadJSON: (url) -> + loadJSON: -> $.ajax do - url : url + url : @get 'url' dataType : 'json' - success : (data) ~> @onLoadSuccess new TimeSeriesData data - error : @onLoadError + success : (data) ~> @onLoadDataSuccess new TimeSeriesData data + error : @onLoadDataError this - loadCSV: (url) -> + loadCSV: -> $.ajax do - url : url + url : @get 'url' dataType : 'text' - success : (data) ~> @onLoadSuccess new CSVData data - error : @onLoadError + success : (data) ~> @onLoadDataSuccess new CSVData data + error : @onLoadDataError this - onLoadSuccess: (@data) -> - console.log "#this.onLoadSuccess #{@data}" - @trigger 'load-success', this - @triggerReady() + onLoadDataSuccess: (@data) -> + console.log "#this.onLoadDataSuccess #{@data}" + @unwait() + @trigger 'load-data-success', this - onLoadError: (jqXHR, txtStatus, err) -> - @_errorLoading = true + onLoadDataError: (jqXHR, txtStatus, err) -> console.error "#this Error loading data! -- #msg: #{err or ''}" - @trigger 'load-error', this, txtStatus, err + @unwait() + @_errorLoading = true + @trigger 'load-data-error', this, txtStatus, err getDateColumn: -> @@ -142,7 +156,7 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{ onMetricChange: -> @metrics.reset @get 'metrics' - + # }}} @@ -159,49 +173,17 @@ DataSourceList = exports.DataSourceList = BaseList.extend do # {{{ +### DataSource Cache -/* * * * DataSource Cache * * * */ - -DataSource import do - CACHE : new DataSourceList - ready : false - - register: (model) -> - return model unless model - # console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model - if @CACHE.contains model - @CACHE.remove model, {+silent} - @CACHE.add model - model - - get: (id) -> - @CACHE.get id - - lookup: (id, cb, cxt=this) -> - # console.log "#{@CACHE}.lookup(#id, #{typeof cb})" - unless @ready - @on 'cache-ready', ~> - @off 'cache-ready', arguments.callee - @lookup id, cb, cxt - return - - if @CACHE.get id - cb.call cxt, null, that - else - Cls = this - @register new Cls {id} - .on 'ready', -> cb.call cxt, null, it - .load() - - -_.bindAll DataSource, 'register', 'get', 'lookup' - +ALL_SOURCES = new DataSourceList +sourceCache = new ModelCache DataSource, {-ready, cache:ALL_SOURCES} # Fetch all DataSources $.getJSON '/datasources/all', (data) -> - DataSource.CACHE.reset _.map data, -> it - DataSource.ready = true - DataSource.trigger 'cache-ready', DataSource + ALL_SOURCES.reset _.map data, op.I + sourceCache.triggerReady() +DataSource.getAllSources = -> + ALL_SOURCES diff --git a/lib/dataset/datasource-ui-view.co b/lib/dataset/datasource-ui-view.co index 5765b41..29df057 100644 --- a/lib/dataset/datasource-ui-view.co +++ b/lib/dataset/datasource-ui-view.co @@ -6,6 +6,7 @@ /** * @class + * Model is a Metric. */ DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{ __bind__ : <[ ]> @@ -52,9 +53,17 @@ DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{ @$el.toggleClass 'in' onSelectMetric: (evt) -> - tr = evt.currentTarget - idx = @$ '.source-metrics .datasource-source-metric' .toArray().indexOf tr - return unless idx is not -1 + # tr = evt.currentTarget + # idx = @$ '.source-metrics .datasource-source-metric' .toArray().indexOf tr + # return unless idx is not -1 + el = $ evt.currentTarget + {source_id, source_col} = el.data() + source_col = parseInt source_col + return if not source_id or isNaN source_col + @$ '.source-metrics .datasource-source-metric' .removeClass 'active' + el.addClass 'active' + @model.set {source_col, source_id} + # @trigger 'edit-metric', @model # }}} diff --git a/lib/dataset/metric-edit-view.co b/lib/dataset/metric-edit-view.co index 47904f4..4cbdcf3 100644 --- a/lib/dataset/metric-edit-view.co +++ b/lib/dataset/metric-edit-view.co @@ -11,13 +11,14 @@ /** * @class + * Model is a Metric. */ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ tagName : 'section' className : 'metric-edit-ui' template : require 'kraken/template/metric-edit' - callOnReturnKeypress : 'onChanged' + callOnReturnKeypress : 'onChange' events: 'keydown .metric-label' : 'onReturnKeypress' @@ -37,18 +38,18 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ @datasource_ui_view = new DataSourceUIView {@model, @graph_id, @dataset, @datasources} @addSubview @datasource_ui_view .on 'update', ~> @trigger 'update', this - @$ '.metric-datasource' .append @datasource_ui_view.render().el + # @$ '.metric-datasource' .append @datasource_ui_view.render().el toTemplateLocals: -> locals = BaseView::toTemplateLocals ... locals import { @graph_id, @dataset, @datasources } - build: -> - BaseView::build ... - if @datasource_ui_view - @$ '.metric-datasource' .append @datasource_ui_view.render().el - this + # build: -> + # BaseView::build ... + # if @datasource_ui_view + # @$ '.metric-datasource' .append @datasource_ui_view.render().el + # this update: -> color = @model.get 'color' @@ -58,16 +59,13 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ this - onChanged: -> + onChange: -> attrs = @$ 'form.metric-edit-form' .formData() @model.set attrs, {+silent} @trigger 'update', this - editMetric: (metric) -> - console.log "#this.editMetric!", metric - @datasource_ui_view.setModel @setModel metric - @render() - @show() + onMetricChange: (metric) -> + console.log "#this.onMetricChange!", metric this # }}} diff --git a/lib/dataset/metric-model.co b/lib/dataset/metric-model.co index b1f7cd2..0810370 100644 --- a/lib/dataset/metric-model.co +++ b/lib/dataset/metric-model.co @@ -9,7 +9,6 @@ DataSource = DataSourceList = null */ Metric = exports.Metric = BaseModel.extend do # {{{ urlRoot : '/metrics' - ready : false /** * Data source of the Metric. @@ -44,9 +43,9 @@ Metric = exports.Metric = BaseModel.extend do # {{{ initialize : -> BaseModel::initialize ... - @on 'change:source_id', @lookupSource, this - @on 'change:source_col', @onUpdateSourceCol, this - @lookupSource() + @on 'change:source_id', @load, this + @on 'change:source_col', @updateId, this + @load() getDateColumn: -> @@ -55,29 +54,32 @@ Metric = exports.Metric = BaseModel.extend do # {{{ getData: -> @source.getColumn @get 'source_col' - lookupSource: -> - if source_id = @get 'source_id' - @wait() - @updateId() - DataSource.lookup source_id, @onSourceReady, this - this - - onSourceReady: (err, source) -> - # console.log "#this.onSourceReady", arguments - @unwait() - if err - console.error "#this Error loading DataSource! #err" - else - @source = source - @updateId() - @triggerReady() - this - - onUpdateSourceCol: -> + load: (opts={}) -> + 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 + + console.log "#this.load()..." @updateId() + @loading = true + @wait() + @trigger 'load', this + + DataSource.lookup source_id, (err, source) ~> + # console.log "#this.onSourceReady", arguments + @loading = false + @unwait() # terminates the `load` wait + if err + console.error "#{this} Error loading DataSource! #err" + else + console.log "#{this}.load() complete!" + @source = source + @updateId() + @triggerReady() this + updateId: -> if (source_id = @get('source_id')) and (source_col = @get('source_col')) @id = "#source_id[#source_col]" diff --git a/lib/graph/graph-edit-view.co b/lib/graph/graph-edit-view.co index e7d403f..2dd3465 100644 --- a/lib/graph/graph-edit-view.co +++ b/lib/graph/graph-edit-view.co @@ -94,31 +94,28 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ .on 'change:dataset', @onModelChange, this .on 'change:options', @onModelChange, this .on 'error', @onModelError, this - .on 'ready', @onReady, this + # .on 'ready', @onReady, this ### Chart Options Tab, Scaffold @scaffold = @addSubview new ChartOptionScaffold - # @$ '.graph-options-pane' .append @scaffold.el @scaffold.collection.reset that if o.graph_spec - @scaffold.on 'change', @onScaffoldChange @chartOptions @model.getOptions(), {+silent} - # 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 - ### Graph Data UI @data_view = @addSubview new DataView { model:@model.get('data'), graph_id:@id } - # @$ '.graph-data-pane' .append @data_view.render().el @data_view - .on 'change', @onDataChange, this .on 'start-waiting', @wait, this .on 'stop-waiting', @unwait, this + .on '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 - @checkWaiting() ### Chart Viewport @resizeViewport() @@ -126,6 +123,28 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ # 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.on 'ready', next.ok .load() + .seq_ (next) ~> + @model.on 'data-ready', next.ok .loadData() + .seq ~> @onReady() + + onReady: -> + return if @ready + console.log "(#this via GraphEditView).ready!" + @unwait() # clears `wait()` from `initialize` + # @ready = @scaffold.ready = true + @triggerReady() + @scaffold.triggerReady() + @chartOptions @model.getOptions(), {+silent} + @render() + + # fix up the spinner element once the DOM is settled + _.delay @checkWaiting, 50 @@ -280,20 +299,21 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ BaseView::attachSubviews ... @checkWaiting() - # render: -> - # return this unless @ready and not @_rendering - # @_rendering = true - # @wait() - # @checkWaiting() - # @renderDetails() - # @attachSubviews() - # # _.invoke @subviews, 'render' - # @renderChart() - # @updateURL() - # @trigger 'render', this - # @unwait() - # @_rendering = false - # this + render: -> + return this unless @ready and not @_rendering + @_rendering = true + @wait() + @checkWaiting() + # @renderDetails() + # @attachSubviews() + # _.invoke @subviews, 'render' + BaseView::render ... + @renderChart() + # @updateURL() + @trigger 'render', this + @unwait() + @_rendering = false + this /** @@ -392,16 +412,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ ### }}} ### Event Handlers {{{ - onReady: -> - return if @ready - console.log "(#this via GraphEditView).ready!" - @ready = @scaffold.ready = true - @unwait() # clears `wait()` from `initialize` - @onSync() - - # fix up the spinner element once the DOM is settled - _.delay @checkWaiting, 50 - onSync: -> return unless @ready console.info "#this.sync() --> success!" diff --git a/lib/graph/graph-model.co b/lib/graph/graph-model.co index d778892..8790325 100644 --- a/lib/graph/graph-model.co +++ b/lib/graph/graph-model.co @@ -29,6 +29,12 @@ Graph = exports.Graph = BaseModel.extend do # {{{ ready : false /** + * Whether this Graph has loaded the actual data needed to draw the chart. + * @type Boolean + */ + dataReady : false + + /** * The chart type backing this graph. * @type ChartType */ @@ -81,48 +87,48 @@ Graph = exports.Graph = BaseModel.extend do # {{{ BaseModel.call this, attributes, opts - initialize: (attributes, opts) -> + initialize: (attributes) -> BaseModel::initialize ... - opts = {+autoload} import (opts or {}) @constructor.register this @parents = new GraphList # TODO: Load on-demand - @chartType = ChartType.lookup @get('chartType') + @chartType = ChartType.lookup @get 'chartType' # Insert submodels in place of JSON - # data = @get('data') or @get('dataset') - @set 'data', @dataset = new DataSet(@get('data')), {+silent} + @dataset = new DataSet {id:@id, ...@get 'data'} + @set 'data', @dataset, {+silent} @trigger 'init', this - @load() if opts.autoload load: (opts={}) -> return this if (@loading or @ready) and not opts.force + console.log "#this.load()..." @loading = true @wait() @trigger 'load', this Seq() + # Fetch model if .seq_ (next) ~> - if @isNew() - next.ok() - else - console.log "#{this}.fetch()..." - @wait() - @fetch do - error : @unwaitAnd (err) ~> - console.error "#{this}.fetch() --> error! #arguments" - next.ok() - success : @unwaitAnd (model, res) ~> - # console.log "#{this}.fetch() --> success!", res - @dataset.set @get('data') - @trigger 'change:data', this, @dataset, 'data' - @trigger 'change', this, @dataset, 'data' - next.ok res + return next.ok() if @isNew() + console.log "#{this}.fetch()..." + @wait() + @fetch do + error : @unwaitAnd (err) ~> + console.error "#{this}.fetch() --> error! #arguments" + next.ok() + success : @unwaitAnd (model, res) ~> + # console.log "#{this}.fetch() --> success!", res + @dataset.set @get 'data' + @trigger 'change:data', this, @dataset, 'data' + @trigger 'change', this, @dataset, 'data' + next.ok res + + # Load Parents... .seq_ (next) ~> - next.ok @get('parents') + next.ok @get 'parents' .flatten() .seqMap_ (next, parent_id) ~> @wait() @@ -132,13 +138,45 @@ Graph = exports.Graph = BaseModel.extend do # {{{ @optionCascade.addLookup parent.get('options') @unwait() next.ok() + + # Load DataSet... + .seq_ (next) ~> + @dataset.once 'ready', next.ok .load() + + # Done! .seq ~> + console.log "#{this}.load() complete!" @loading = false @unwait() # terminates the `load` wait @triggerReady() this + loadData: (opts={}) -> + @resetReady 'dataReady', 'data-ready' if opts.force + return this if @loading or @dataReady + + unless @dataset.metrics.length + return @triggerReady 'dataReady', 'data-ready' + + console.log "#this.loadData()..." + @wait() + @loading = true + @trigger 'load-data', this + 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() + .seq ~> + console.log "#{this}.loadData() complete!" + @loading = false + @unwait() # terminates the `load` wait + @triggerReady 'dataReady', 'data-ready' + this + + ### Accessors @@ -308,7 +346,10 @@ Graph = exports.Graph = BaseModel.extend do # {{{ toPermalink: -> "#{root.location.protocol}//#{window.location.host}#{@toLink()}" - + +### Graph Cache for parent-lookup +new ModelCache Graph + # }}} @@ -318,15 +359,8 @@ GraphList = exports.GraphList = BaseList.extend do # {{{ constructor : function GraphList then BaseList ... initialize : -> BaseList::initialize ... - - toString: -> - modelIds = @models - .map -> "\"#{it.id ? it.cid}\"" - .join ', ' - "#{@getClassName()}(#modelIds)" + toString: -> @toStringWithIds() # }}} -### Graph Cache for parent-lookup -new ModelCache Graph diff --git a/lib/main-edit.co b/lib/main-edit.co index d020caa..64b54cf 100644 --- a/lib/main-edit.co +++ b/lib/main-edit.co @@ -3,15 +3,9 @@ Backbone = require 'backbone' { _, op, } = require 'kraken/util' -{ TimeSeriesData, CSVData, -} = require 'kraken/timeseries' { BaseView, BaseModel, BaseList, } = require 'kraken/base' -{ Field, FieldList, FieldView, Scaffold, -} = require 'kraken/scaffold' { ChartType, DygraphsChartType, - ChartOption, ChartOptionList, TagSet, - ChartOptionView, ChartOptionScaffold, } = require 'kraken/chart' { DataSource, DataSourceList, } = require 'kraken/dataset' @@ -75,6 +69,5 @@ Seq([ <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]> next.ok() error : (err) -> console.error err .seq -> - console.log 'All data loaded!' jQuery main diff --git a/lib/template/datasource-ui.jade b/lib/template/datasource-ui.jade index 13c2ec5..f04e330 100644 --- a/lib/template/datasource-ui.jade +++ b/lib/template/datasource-ui.jade @@ -1,6 +1,6 @@ -section.datasource-ui +section.datasource-ui(class="datasource-ui-#{source_id}") - section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui .datasource-selector") + section.datasource-summary(data-toggle="collapse", data-target="##{graph_id} .datasource-ui.datasource-ui-#{source_id} .datasource-selector") i.expand-datasource-ui-button.icon-chevron-down i.collapse-datasource-ui-button.icon-chevron-up @@ -48,7 +48,7 @@ section.datasource-ui tbody.source-metrics for m, idx in ds.metrics.slice(1) - var activeColClass = (activeClass && source_col === m.idx) ? 'active' : '' - tr.datasource-source-metric(class=activeColClass) + tr.datasource-source-metric(class=activeColClass, data-source_id=ds.id, data-source_col=m.idx) td.source-metric-idx #{m.idx} td.source-metric-label #{m.label} td.source-metric-type #{m.type} diff --git a/lib/util/backbone.co b/lib/util/backbone.co index 58f2688..f87fda5 100644 --- a/lib/util/backbone.co +++ b/lib/util/backbone.co @@ -13,12 +13,26 @@ Backbone.setDomLibrary that if window? and (window.jQuery or window.Zepto or win _bb_events = + + /** + * Registers an event listener on the given event(s) to be fired only once. + * + * @param {String} events Space delimited list of event names. + * @param {Function} callback Event listener function. + * @param {Object} [context=this] Object to be supplied as the context for the listener. + * @returns {this} + */ once: (events, callback, context) -> fn = ~> @off events, arguments.callee, this callback.apply (context or this), arguments @on events, fn, this - fn + this + + /** + * Compatibility with Node's `EventEmitter`. + */ + emit: Backbone.Events.trigger diff --git a/lib/util/event/ready-emitter.co b/lib/util/event/ready-emitter.co index fb24be4..d3f893e 100644 --- a/lib/util/event/ready-emitter.co +++ b/lib/util/event/ready-emitter.co @@ -1,4 +1,5 @@ {EventEmitter} = require 'events' +EventEmitter::trigger = EventEmitter::emit /** diff --git a/lib/util/event/waiting-emitter.co b/lib/util/event/waiting-emitter.co index fc98aa6..3cc2733 100644 --- a/lib/util/event/waiting-emitter.co +++ b/lib/util/event/waiting-emitter.co @@ -1,4 +1,6 @@ {EventEmitter} = require 'events' +EventEmitter::trigger = EventEmitter::emit + /** -- 1.7.0.4