From acb540386ad679413fb84305cb438228707f8cdf Mon Sep 17 00:00:00 2001 From: David Schoonover Date: Tue, 10 Jul 2012 04:30:14 -0700 Subject: [PATCH] Moves Coco source tree from 'lib' to 'src' -- compiled JS files will live in 'lib'. --- lib/app.co | 51 ---- lib/base/asset-manager.co | 43 --- lib/base/base-mixin.co | 221 --------------- lib/base/base-model.co | 252 ----------------- lib/base/base-view.co | 293 ------------------- lib/base/base.co | 74 ----- lib/base/cascading-model.co | 57 ---- lib/base/data-binding.co | 67 ----- lib/base/index.co | 9 - lib/base/model-cache.co | 196 ------------- lib/base/scaffold/index.co | 3 - lib/base/scaffold/scaffold-model.co | 105 ------- lib/base/scaffold/scaffold-view.co | 125 --------- lib/chart/chart-type.co | 425 ---------------------------- lib/chart/index.co | 8 - lib/chart/option/chart-option-model.co | 218 --------------- lib/chart/option/chart-option-view.co | 270 ------------------ lib/chart/option/index.co | 4 - lib/chart/type/d3-chart.co | 138 --------- lib/chart/type/d3/d3-bar-chart-type.co | 239 ---------------- lib/chart/type/d3/d3-bar-element.co | 78 ----- lib/chart/type/d3/d3-chart-element.co | 84 ------ lib/chart/type/d3/d3-geo-element.co | 185 ------------ lib/chart/type/d3/d3-line-element.co | 111 -------- lib/chart/type/d3/index.co | 6 - lib/chart/type/dygraphs.co | 133 --------- lib/dashboard/dashboard-model.co | 87 ------ lib/dashboard/dashboard-view.co | 174 ------------ lib/dashboard/index.co | 3 - lib/data/data-view.co | 122 -------- lib/data/dataset-model.co | 183 ------------ lib/data/dataset-view.co | 155 ---------- lib/data/datasource-model.co | 190 ------------- lib/data/datasource-ui-view.co | 69 ----- lib/data/datasource-view.co | 24 -- lib/data/index.co | 13 - lib/data/metric-edit-view.co | 94 ------- lib/data/metric-model.co | 175 ------------ lib/data/project-colors.co | 37 --- lib/graph/graph-display-view.co | 88 ------ lib/graph/graph-edit-view.co | 223 --------------- lib/graph/graph-list-view.co | 36 --- lib/graph/graph-model.co | 433 ----------------------------- lib/graph/graph-view.co | 299 -------------------- lib/graph/index.co | 7 - lib/main-dashboard.co | 37 --- lib/main-display.co | 53 ---- lib/main-edit.co | 53 ---- lib/main-geo.co | 181 ------------ lib/main-graph-list.co | 26 -- lib/server/controller.co | 105 ------- lib/server/controllers/dashboard.co | 91 ------ lib/server/controllers/datasource.co | 127 --------- lib/server/controllers/graph.co | 153 ---------- lib/server/file-controller.co | 95 ------- lib/server/files.co | 96 ------- lib/server/index.js | 8 - lib/server/middleware.co | 257 ----------------- lib/server/mkdirp.co | 46 --- lib/server/proxy.co | 37 --- lib/server/reqinfo.co | 20 -- lib/server/server.co | 58 ---- lib/server/view-helpers.co | 52 ---- lib/template/browser-helpers.jade | 3 - lib/template/chart/chart-option.jade | 41 --- lib/template/chart/chart-scaffold.jade | 12 - lib/template/dashboard/dashboard-tab.jade | 1 - lib/template/dashboard/dashboard.jade | 17 -- lib/template/data/data.jade | 4 - lib/template/data/dataset-metric.jade | 8 - lib/template/data/dataset.jade | 24 -- lib/template/data/datasource-ui.jade | 55 ---- lib/template/data/datasource.jade | 29 -- lib/template/data/metric-edit.jade | 22 -- lib/template/graph/graph-display.jade | 37 --- lib/template/graph/graph-edit.jade | 76 ----- lib/template/graph/graph-list.jade | 7 - lib/util/aliasdict.co | 158 ----------- lib/util/backbone.co | 128 --------- lib/util/bitstring.co | 247 ---------------- lib/util/cascade.co | 379 ------------------------- lib/util/crc.co | 58 ---- lib/util/event/index.co | 2 - lib/util/event/ready-emitter.co | 54 ---- lib/util/event/waiting-emitter.co | 55 ---- lib/util/formatters.co | 68 ----- lib/util/hashset.co | 418 ---------------------------- lib/util/index.co | 38 --- lib/util/op.co | 154 ---------- lib/util/parser.co | 127 --------- lib/util/timeseries/csv.co | 115 -------- lib/util/timeseries/index.co | 2 - lib/util/timeseries/timeseries.co | 191 ------------- lib/util/underscore/_functions.co | 220 --------------- lib/util/underscore/array.co | 48 ---- lib/util/underscore/class.co | 53 ---- lib/util/underscore/function.co | 27 -- lib/util/underscore/index.co | 29 -- lib/util/underscore/kv.co | 60 ---- lib/util/underscore/object.co | 270 ------------------ lib/util/underscore/string.co | 75 ----- src/app.co | 51 ++++ src/base/asset-manager.co | 43 +++ src/base/base-mixin.co | 221 +++++++++++++++ src/base/base-model.co | 252 +++++++++++++++++ src/base/base-view.co | 293 +++++++++++++++++++ src/base/base.co | 74 +++++ src/base/cascading-model.co | 57 ++++ src/base/data-binding.co | 67 +++++ src/base/index.co | 9 + src/base/model-cache.co | 196 +++++++++++++ src/base/scaffold/index.co | 3 + src/base/scaffold/scaffold-model.co | 105 +++++++ src/base/scaffold/scaffold-view.co | 125 +++++++++ src/chart/chart-type.co | 425 ++++++++++++++++++++++++++++ src/chart/index.co | 8 + src/chart/option/chart-option-model.co | 218 +++++++++++++++ src/chart/option/chart-option-view.co | 270 ++++++++++++++++++ src/chart/option/index.co | 4 + src/chart/type/d3-chart.co | 138 +++++++++ src/chart/type/d3/d3-bar-chart-type.co | 239 ++++++++++++++++ src/chart/type/d3/d3-bar-element.co | 78 +++++ src/chart/type/d3/d3-chart-element.co | 84 ++++++ src/chart/type/d3/d3-geo-element.co | 185 ++++++++++++ src/chart/type/d3/d3-line-element.co | 111 ++++++++ src/chart/type/d3/index.co | 6 + src/chart/type/dygraphs.co | 133 +++++++++ src/dashboard/dashboard-model.co | 87 ++++++ src/dashboard/dashboard-view.co | 174 ++++++++++++ src/dashboard/index.co | 3 + src/data/data-view.co | 122 ++++++++ src/data/dataset-model.co | 183 ++++++++++++ src/data/dataset-view.co | 155 ++++++++++ src/data/datasource-model.co | 190 +++++++++++++ src/data/datasource-ui-view.co | 69 +++++ src/data/datasource-view.co | 24 ++ src/data/index.co | 13 + src/data/metric-edit-view.co | 94 +++++++ src/data/metric-model.co | 175 ++++++++++++ src/data/project-colors.co | 37 +++ src/graph/graph-display-view.co | 88 ++++++ src/graph/graph-edit-view.co | 223 +++++++++++++++ src/graph/graph-list-view.co | 36 +++ src/graph/graph-model.co | 433 +++++++++++++++++++++++++++++ src/graph/graph-view.co | 299 ++++++++++++++++++++ src/graph/index.co | 7 + src/main-dashboard.co | 37 +++ src/main-display.co | 53 ++++ src/main-edit.co | 53 ++++ src/main-geo.co | 181 ++++++++++++ src/main-graph-list.co | 26 ++ src/server/controller.co | 105 +++++++ src/server/controllers/dashboard.co | 91 ++++++ src/server/controllers/datasource.co | 127 +++++++++ src/server/controllers/graph.co | 153 ++++++++++ src/server/file-controller.co | 95 +++++++ src/server/files.co | 96 +++++++ src/server/index.js | 8 + src/server/middleware.co | 257 +++++++++++++++++ src/server/mkdirp.co | 46 +++ src/server/proxy.co | 37 +++ src/server/reqinfo.co | 20 ++ src/server/server.co | 58 ++++ src/server/view-helpers.co | 52 ++++ src/template/browser-helpers.jade | 3 + src/template/chart/chart-option.jade | 41 +++ src/template/chart/chart-scaffold.jade | 12 + src/template/dashboard/dashboard-tab.jade | 1 + src/template/dashboard/dashboard.jade | 17 ++ src/template/data/data.jade | 4 + src/template/data/dataset-metric.jade | 8 + src/template/data/dataset.jade | 24 ++ src/template/data/datasource-ui.jade | 55 ++++ src/template/data/datasource.jade | 29 ++ src/template/data/metric-edit.jade | 22 ++ src/template/graph/graph-display.jade | 37 +++ src/template/graph/graph-edit.jade | 76 +++++ src/template/graph/graph-list.jade | 7 + src/util/aliasdict.co | 158 +++++++++++ src/util/backbone.co | 128 +++++++++ src/util/bitstring.co | 247 ++++++++++++++++ src/util/cascade.co | 379 +++++++++++++++++++++++++ src/util/crc.co | 58 ++++ src/util/event/index.co | 2 + src/util/event/ready-emitter.co | 54 ++++ src/util/event/waiting-emitter.co | 55 ++++ src/util/formatters.co | 68 +++++ src/util/hashset.co | 418 ++++++++++++++++++++++++++++ src/util/index.co | 38 +++ src/util/op.co | 154 ++++++++++ src/util/parser.co | 127 +++++++++ src/util/timeseries/csv.co | 115 ++++++++ src/util/timeseries/index.co | 2 + src/util/timeseries/timeseries.co | 191 +++++++++++++ src/util/underscore/_functions.co | 220 +++++++++++++++ src/util/underscore/array.co | 48 ++++ src/util/underscore/class.co | 53 ++++ src/util/underscore/function.co | 27 ++ src/util/underscore/index.co | 29 ++ src/util/underscore/kv.co | 60 ++++ src/util/underscore/object.co | 270 ++++++++++++++++++ src/util/underscore/string.co | 75 +++++ 202 files changed, 10614 insertions(+), 10614 deletions(-) delete mode 100644 lib/app.co delete mode 100644 lib/base/asset-manager.co delete mode 100644 lib/base/base-mixin.co delete mode 100644 lib/base/base-model.co delete mode 100644 lib/base/base-view.co delete mode 100644 lib/base/base.co delete mode 100644 lib/base/cascading-model.co delete mode 100644 lib/base/data-binding.co delete mode 100644 lib/base/index.co delete mode 100644 lib/base/model-cache.co delete mode 100644 lib/base/scaffold/index.co delete mode 100644 lib/base/scaffold/scaffold-model.co delete mode 100644 lib/base/scaffold/scaffold-view.co delete mode 100644 lib/chart/chart-type.co delete mode 100644 lib/chart/index.co delete mode 100644 lib/chart/option/chart-option-model.co delete mode 100644 lib/chart/option/chart-option-view.co delete mode 100644 lib/chart/option/index.co delete mode 100644 lib/chart/type/d3-chart.co delete mode 100644 lib/chart/type/d3/d3-bar-chart-type.co delete mode 100644 lib/chart/type/d3/d3-bar-element.co delete mode 100644 lib/chart/type/d3/d3-chart-element.co delete mode 100644 lib/chart/type/d3/d3-geo-element.co delete mode 100644 lib/chart/type/d3/d3-line-element.co delete mode 100644 lib/chart/type/d3/index.co delete mode 100644 lib/chart/type/dygraphs.co delete mode 100644 lib/chart/type/index.co delete mode 100644 lib/dashboard/dashboard-model.co delete mode 100644 lib/dashboard/dashboard-view.co delete mode 100644 lib/dashboard/index.co delete mode 100644 lib/data/data-view.co delete mode 100644 lib/data/dataset-model.co delete mode 100644 lib/data/dataset-view.co delete mode 100644 lib/data/datasource-model.co delete mode 100644 lib/data/datasource-ui-view.co delete mode 100644 lib/data/datasource-view.co delete mode 100644 lib/data/index.co delete mode 100644 lib/data/metric-edit-view.co delete mode 100644 lib/data/metric-model.co delete mode 100644 lib/data/project-colors.co delete mode 100644 lib/graph/graph-display-view.co delete mode 100644 lib/graph/graph-edit-view.co delete mode 100644 lib/graph/graph-list-view.co delete mode 100644 lib/graph/graph-model.co delete mode 100644 lib/graph/graph-view.co delete mode 100644 lib/graph/index.co delete mode 100644 lib/main-dashboard.co delete mode 100644 lib/main-display.co delete mode 100644 lib/main-edit.co delete mode 100644 lib/main-geo.co delete mode 100644 lib/main-graph-list.co delete mode 100644 lib/server/controller.co delete mode 100644 lib/server/controllers/dashboard.co delete mode 100644 lib/server/controllers/datasource.co delete mode 100644 lib/server/controllers/graph.co delete mode 100644 lib/server/controllers/index.co delete mode 100644 lib/server/file-controller.co delete mode 100644 lib/server/files.co delete mode 100755 lib/server/index.js delete mode 100755 lib/server/middleware.co delete mode 100644 lib/server/mkdirp.co delete mode 100644 lib/server/proxy.co delete mode 100644 lib/server/reqinfo.co delete mode 100755 lib/server/server.co delete mode 100644 lib/server/view-helpers.co delete mode 100644 lib/template/browser-helpers.jade delete mode 100644 lib/template/chart/chart-option.jade delete mode 100644 lib/template/chart/chart-scaffold.jade delete mode 100644 lib/template/dashboard/dashboard-tab.jade delete mode 100644 lib/template/dashboard/dashboard.jade delete mode 100644 lib/template/data/data.jade delete mode 100644 lib/template/data/dataset-metric.jade delete mode 100644 lib/template/data/dataset.jade delete mode 100644 lib/template/data/datasource-ui.jade delete mode 100644 lib/template/data/datasource.jade delete mode 100644 lib/template/data/metric-edit.jade delete mode 100644 lib/template/graph/graph-display.jade delete mode 100644 lib/template/graph/graph-edit.jade delete mode 100644 lib/template/graph/graph-list.jade delete mode 100644 lib/util/aliasdict.co delete mode 100644 lib/util/backbone.co delete mode 100644 lib/util/bitstring.co delete mode 100644 lib/util/cascade.co delete mode 100644 lib/util/crc.co delete mode 100644 lib/util/event/index.co delete mode 100644 lib/util/event/ready-emitter.co delete mode 100644 lib/util/event/waiting-emitter.co delete mode 100644 lib/util/formatters.co delete mode 100644 lib/util/hashset.co delete mode 100644 lib/util/index.co delete mode 100644 lib/util/op.co delete mode 100644 lib/util/parser.co delete mode 100644 lib/util/timeseries/csv.co delete mode 100644 lib/util/timeseries/index.co delete mode 100644 lib/util/timeseries/timeseries.co delete mode 100644 lib/util/underscore/_functions.co delete mode 100644 lib/util/underscore/array.co delete mode 100644 lib/util/underscore/class.co delete mode 100644 lib/util/underscore/function.co delete mode 100644 lib/util/underscore/index.co delete mode 100644 lib/util/underscore/kv.co delete mode 100644 lib/util/underscore/object.co delete mode 100644 lib/util/underscore/string.co create mode 100644 src/app.co create mode 100644 src/base/asset-manager.co create mode 100644 src/base/base-mixin.co create mode 100644 src/base/base-model.co create mode 100644 src/base/base-view.co create mode 100644 src/base/base.co create mode 100644 src/base/cascading-model.co create mode 100644 src/base/data-binding.co create mode 100644 src/base/index.co create mode 100644 src/base/model-cache.co create mode 100644 src/base/scaffold/index.co create mode 100644 src/base/scaffold/scaffold-model.co create mode 100644 src/base/scaffold/scaffold-view.co create mode 100644 src/chart/chart-type.co create mode 100644 src/chart/index.co create mode 100644 src/chart/option/chart-option-model.co create mode 100644 src/chart/option/chart-option-view.co create mode 100644 src/chart/option/index.co create mode 100644 src/chart/type/d3-chart.co create mode 100644 src/chart/type/d3/d3-bar-chart-type.co create mode 100644 src/chart/type/d3/d3-bar-element.co create mode 100644 src/chart/type/d3/d3-chart-element.co create mode 100644 src/chart/type/d3/d3-geo-element.co create mode 100644 src/chart/type/d3/d3-line-element.co create mode 100644 src/chart/type/d3/index.co create mode 100644 src/chart/type/dygraphs.co create mode 100644 src/chart/type/index.co create mode 100644 src/dashboard/dashboard-model.co create mode 100644 src/dashboard/dashboard-view.co create mode 100644 src/dashboard/index.co create mode 100644 src/data/data-view.co create mode 100644 src/data/dataset-model.co create mode 100644 src/data/dataset-view.co create mode 100644 src/data/datasource-model.co create mode 100644 src/data/datasource-ui-view.co create mode 100644 src/data/datasource-view.co create mode 100644 src/data/index.co create mode 100644 src/data/metric-edit-view.co create mode 100644 src/data/metric-model.co create mode 100644 src/data/project-colors.co create mode 100644 src/graph/graph-display-view.co create mode 100644 src/graph/graph-edit-view.co create mode 100644 src/graph/graph-list-view.co create mode 100644 src/graph/graph-model.co create mode 100644 src/graph/graph-view.co create mode 100644 src/graph/index.co create mode 100644 src/main-dashboard.co create mode 100644 src/main-display.co create mode 100644 src/main-edit.co create mode 100644 src/main-geo.co create mode 100644 src/main-graph-list.co create mode 100644 src/server/controller.co create mode 100644 src/server/controllers/dashboard.co create mode 100644 src/server/controllers/datasource.co create mode 100644 src/server/controllers/graph.co create mode 100644 src/server/controllers/index.co create mode 100644 src/server/file-controller.co create mode 100644 src/server/files.co create mode 100755 src/server/index.js create mode 100755 src/server/middleware.co create mode 100644 src/server/mkdirp.co create mode 100644 src/server/proxy.co create mode 100644 src/server/reqinfo.co create mode 100755 src/server/server.co create mode 100644 src/server/view-helpers.co create mode 100644 src/template/browser-helpers.jade create mode 100644 src/template/chart/chart-option.jade create mode 100644 src/template/chart/chart-scaffold.jade create mode 100644 src/template/dashboard/dashboard-tab.jade create mode 100644 src/template/dashboard/dashboard.jade create mode 100644 src/template/data/data.jade create mode 100644 src/template/data/dataset-metric.jade create mode 100644 src/template/data/dataset.jade create mode 100644 src/template/data/datasource-ui.jade create mode 100644 src/template/data/datasource.jade create mode 100644 src/template/data/metric-edit.jade create mode 100644 src/template/graph/graph-display.jade create mode 100644 src/template/graph/graph-edit.jade create mode 100644 src/template/graph/graph-list.jade create mode 100644 src/util/aliasdict.co create mode 100644 src/util/backbone.co create mode 100644 src/util/bitstring.co create mode 100644 src/util/cascade.co create mode 100644 src/util/crc.co create mode 100644 src/util/event/index.co create mode 100644 src/util/event/ready-emitter.co create mode 100644 src/util/event/waiting-emitter.co create mode 100644 src/util/formatters.co create mode 100644 src/util/hashset.co create mode 100644 src/util/index.co create mode 100644 src/util/op.co create mode 100644 src/util/parser.co create mode 100644 src/util/timeseries/csv.co create mode 100644 src/util/timeseries/index.co create mode 100644 src/util/timeseries/timeseries.co create mode 100644 src/util/underscore/_functions.co create mode 100644 src/util/underscore/array.co create mode 100644 src/util/underscore/class.co create mode 100644 src/util/underscore/function.co create mode 100644 src/util/underscore/index.co create mode 100644 src/util/underscore/kv.co create mode 100644 src/util/underscore/object.co create mode 100644 src/util/underscore/string.co diff --git a/lib/app.co b/lib/app.co deleted file mode 100644 index a4185b0..0000000 --- a/lib/app.co +++ /dev/null @@ -1,51 +0,0 @@ -Backbone = require 'backbone' - -{ _, op, -} = require 'kraken/util' - - -/** - * @class Application view, automatically attaching to an existing element - * found at `appSelector`. - * @extends Backbone.View - */ -AppView = exports.AppView = Backbone.View.extend do # {{{ - appSelector : '#content .inner' - - - /** - * @constructor - */ - constructor: function AppView (options={}) - if typeof options is 'function' - @initialize = options - options = {} - else - @initialize = that if options.initialize - - @appSelector = that if options.appSelector - options.el or= jQuery @appSelector .0 - Backbone.View.call this, options - - jQuery ~> @render() - this - - /** - * Override to set up your app. This method may be passed - * as an option to the constructor. - */ - initialize: -> # stub - - /** - * Append subviews. - */ - render : -> - @$el.append @view.el if @view and not @view.$el.parent()?.length - - getClassName: -> - "#{@..name or @..displayName}" - - toString: -> - "#{@getClassName()}()" -# }}} - diff --git a/lib/base/asset-manager.co b/lib/base/asset-manager.co deleted file mode 100644 index fa91012..0000000 --- a/lib/base/asset-manager.co +++ /dev/null @@ -1,43 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ ReadyEmitter, -} = require 'kraken/util/event' - - - - -class AssetManager extends ReadyEmitter - # Map from key/url to data. - assets : null - - - /** - * @constructor - */ - -> - super ... - @assets = {} - - - - - /** - * Load the corresponding chart specification, which includes - * info about valid options, along with their types and defaults. - */ - load: -> - return this if @ready - proto = @constructor:: - jQuery.ajax do - url : @SPEC_URL - success : (spec) ~> - proto.spec = spec - proto.options_ordered = spec - proto.options = _.synthesize spec, -> [it.name, it] - proto.ready = true - @emit 'ready', this - error: ~> console.error "Error loading #{@typeName} spec! #it" - this - - - diff --git a/lib/base/base-mixin.co b/lib/base/base-mixin.co deleted file mode 100644 index c6764cb..0000000 --- a/lib/base/base-mixin.co +++ /dev/null @@ -1,221 +0,0 @@ -Backbone = require 'backbone' - -{ _, op, -} = require 'kraken/util' - - - -BaseBackboneMixin = exports.BaseBackboneMixin = - - initialize: -> - @__apply_bind__() - - - ### Auto-Bound methods - - /** - * A list of method-names to bind on `initialize`; set this on a subclass to override. - * @type Array - */ - __bind__ : [] - - /** - * Applies the contents of `__bind__`. - */ - __apply_bind__: -> - names = _ @pluckSuperAndSelf '__bind__' .chain().flatten().compact().unique().value() - _.bindAll this, ...names if names.length - - - - - ### Events - - /** - * Whether we're ready. - * @type Boolean - */ - ready : false - - - /** - * Triggers the 'ready' event if it has not yet been triggered. - * Subsequent listeners added on this event will be auto-triggered. - * @returns {this} - */ - triggerReady: (lock='ready', event='ready') -> - return this if @[lock] - @[lock] = true - @trigger event, this - this - - /** - * Resets the 'ready' event to its non-triggered state, firing a - * 'ready-reset' event. - * @returns {this} - */ - resetReady: (lock='ready', event='ready') -> - return this unless @[lock] - @[lock] = false - @trigger "#event-reset", this - this - - /** - * Wrap {@link Backbone.Event#on} registration to handle registrations - * on 'ready' after we've broadcast the event. Handler will always still - * be registered, however, in case the emitter is reset. - * - * @param {String} events Space-separated events for which to register. - * @param {Function} callback - * @param {Object} [context] - * @returns {this} - */ - on: (events, callback, context=this) -> - return this if not callback - Backbone.Events.on ... - if @ready and _.contains events.split(/\s+/), 'ready' - callback.call context, this - this - - makeHandlersForCallback: (cb) -> - success : ~> cb.call this, [null].concat arguments - error : ~> cb.call this, it - - - - - ### Synchronization - - /** - * Count of outstanding tasks. - * @type Number - */ - waitingOn : 0 - - - /** - * Increment the waiting task counter. - * @returns {this} - */ - wait: -> - count = @waitingOn - @waitingOn += 1 - # console.log "#this.wait! #count --> #{@waitingOn}" - # console.trace() - @trigger('start-waiting', this) if count is 0 and @waitingOn > 0 - this - - /** - * Decrement the waiting task counter. - * @returns {this} - */ - unwait: -> - count = @waitingOn - @waitingOn -= 1 - # console.warn "#this.unwait! #{@waitingOn} < 0" if @waitingOn < 0 - # console.log "#this.unwait! #count --> #{@waitingOn}" - # console.trace() - @trigger('stop-waiting', this) if @waitingOn is 0 and count > 0 - this - - /** - * @param {Function} fn Function to wrap. - * @returns {Function} A function wrapping the passed function with a call - * to `unwait()`, then delegating with current context and arguments. - */ - unwaitAnd: (fn) -> - self = this - -> - # console.log "#self.unwaitAnd( function #{fn.name or fn.displayName}() )" - # console.trace() - self.unwait(); fn ... - - - - ### - - getClassName: -> - "#{@..name or @..displayName}" - - toString: -> - "#{@getClassName()}()" - - - - -/** - * @class Base mixin class. Extend this to create a new mixin, attaching the - * donor methods as you would instance methods. - * - * To mingle your mixin with another class or object: - * - * class MyMixin extends Mixin - * foo: -> "foo!" - * - * # Mix into an object... - * o = MyMixin.mix { bar:1 } - * - * # Mix into a Coco class... - * class Bar - * MyMixin.mix this - * bar : 1 - * - */ -class exports.Mixin - - /** - * Mixes this mixin into the target. If `target` is not a class, a new - * object will be returned which inherits from the mixin. - */ - @mix = (target) -> - return that unless target - - MixinClass = Mixin - MixinClass = @constructor if this instanceof Mixin - MixinClass = this if this instanceof Function - - if typeof target is 'function' - target:: import MixinClass:: - else - target = _.clone(MixinClass::) import target - - (target.__mixins__ or= []).push MixinClass - target - - /** - * Coco metaprogramming hook to propagate class properties and methods. - */ - @extended = (SubClass) -> - SuperClass = this - for own k, v in SuperClass - SubClass[k] = v unless SubClass[k] - SubClass - - - -# /** -# * @returns {Function} Function which takes a target object or class, -# * mixes the MixinClass into it, and then returns it. If the target is -# * not a class, a new object will be returned which inherits from the mixin. -# */ -# makeMixer = exports.makeMixer = (MixinClass) -> -# mixinBody = if typeof MixinClass is 'function' then MixinClass:: else MixinClass -# mixinMixer = (target) -> -# if typeof target is 'function' -# target:: import mixinBody -# else -# target = _.clone(mixinBody) import target -# target -# -# mixinBase = exports.mixinBase = makeMixer BaseBackboneMixin - - -/** - * Mixes BaseBackboneMixin into another object or prototype. - * @returns {Object} The merged prototype object. - */ -mixinBase = exports.mixinBase = (...bodies) -> - _.extend _.clone(BaseBackboneMixin), ...bodies - - - diff --git a/lib/base/base-model.co b/lib/base/base-model.co deleted file mode 100644 index 54b725e..0000000 --- a/lib/base/base-model.co +++ /dev/null @@ -1,252 +0,0 @@ -Backbone = require 'backbone' - -{ _, op, -} = require 'kraken/util' -{ BaseBackboneMixin, mixinBase, -} = require 'kraken/base/base-mixin' - - - -/** - * @class Base model, extending Backbone.Model, used by scaffold and others. - * @extends Backbone.Model - */ -BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ - - constructor : function BaseModel - @__class__ = @constructor - @__superclass__ = @..__super__.constructor - @waitingOn = 0 - Backbone.Model ... - # @..trigger 'create', this - - - - - ### Accessors - - url: -> - "#{@urlRoot}/#{@get('id')}.json" - - has: (key) -> - @get(key)? - - get: (key) -> - _.getNested @attributes, key - - # TODO: nested sets, handling events - - # set: (key, value, opts) -> - # if _.isObject(key) and key? - # [values, opts] = [key, value] - # else - # values = { "#key": value } - # - # # TODO: Validation - # @_changed or= {} - # - # for key, value in values - # if _.str.contains key, '.' - # _.setNested @attributes, key, value, opts - # else - # Backbone.Model::set.call this, key, value, opts - # - # this - # - # unset : (key, opts) -> - # - - - ### 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 successfully. - * @param {String} [opts.errorEvent='load-error'] Event which signals loading has completed but failed. - * @param {Boolean} [opts.force=false] If true, reset ready state if we're ready before proceeding. - * @param {Boolean} [opts.readyIfError=false] If true, move fire the ready event when loading completes, even if it failed. - * @returns {this} - */ - loader: (opts={}) -> - opts = { - -force - -readyIfError - startEvent : 'load' - completeEvent : 'load-success' - errorEvent : 'load-error' - ...opts - } - @resetReady() if opts.force - throw new Error('You must specify a `start` function to start loading!') unless opts.start - return this if @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() - @once opts.errorEvent, ~> - # console.log "#{this}.onLoadError()" - @loading = false - @unwait() # terminates the `load` wait - @trigger 'load-error', this unless opts.errorEvent is 'load-error' - @triggerReady() if opts.readyIfError - - # 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 - - serialize: (v) -> - # if v!? - # v = '' - if _.isBoolean v - v = Number v - else if _.isObject v - v = JSON.stringify v - String v - - /** - * Like `.toJSON()` in that it should return a plain object with no functions, - * but for the purpose of `.toKV()`, allowing you to customize the values - * included and keys used. - * - * @param {Object} [opts={}] Options: - * @param {Boolean} [opts.keepFunctions=false] If false, functions will be omitted from the result. - * @returns {Object} - */ - toKVPairs: (opts={}) -> - opts = {-keepFunctions, ...opts} - kvo = _.collapseObject @toJSON() - for k, v in kvo - kvo[k] = @serialize v if opts.keepFunctions or typeof v is not 'function' - kvo - - /** - * Serialize the model into a `www-form-encoded` string suitable for use as - * a query string or a POST body. - * @returns {String} - */ - toKV: (item_delim='&', kv_delim='=') -> - _.toKV @toKVPairs(), item_delim, kv_delim - - /** - * @returns {String} URL identifying this model. - */ - toURL: -> - "?#{@toKV ...}" - - toString: -> - "#{@getClassName()}(cid=#{@cid}, id=#{@id})" - - -# Class Methods -BaseModel import do - /** - * Factory method which constructs an instance of this model from a string of KV-pairs. - * This is a class method inherited by models which extend {BaseModel}. - * @static - * @param {String|Object} o Serialized KV-pairs (or a plain object). - * @returns {BaseModel} An instance of this model. - */ - fromKV: (o, item_delim='&', kv_delim='=') -> - o = _.fromKV o, item_delim, kv_delim if typeof o is 'string' - Cls = if typeof this is 'function' then this else this.constructor - new Cls _.uncollapseObject o - -# }}} - - - -/** - * @class Base collection, extending Backbone.Collection, used by scaffold and others. - * @extends Backbone.Collection - */ -BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{ - - constructor : function BaseList - @__class__ = @constructor - @__superclass__ = @..__super__.constructor - @waitingOn = 0 - Backbone.Collection ... - # @trigger 'create', this - - - getIds: -> - @models.map -> it.id or it.get('id') or it.cid - - - ### Serialization - - toKVPairs: -> - _.collapseObject @toJSON() - - toKV: (item_delim='&', kv_delim='=') -> - _.toKV @toKVPairs(), item_delim, kv_delim - - toURL: (item_delim='&', kv_delim='=') -> - "?#{@toKV ...}" - - toString: -> - "#{@getClassName()}[#{@length}]" - - toStringWithIds: -> - modelIds = @models - .map -> "\"#{it.id ? it.cid}\"" - .join ', ' - "#{@getClassName()}[#{@length}](#modelIds)" -# }}} - - diff --git a/lib/base/base-view.co b/lib/base/base-view.co deleted file mode 100644 index 18646a5..0000000 --- a/lib/base/base-view.co +++ /dev/null @@ -1,293 +0,0 @@ -Backbone = require 'backbone' - -{ _, op, -} = require 'kraken/util' -{ BaseBackboneMixin, mixinBase, -} = require 'kraken/base/base-mixin' -{ BaseModel, -} = require 'kraken/base/base-mixin' -{ DataBinding, -} = require 'kraken/base/data-binding' - - - -/** - * @class Base view, extending Backbone.View, used by scaffold and others. - * @extends Backbone.View - */ -BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ - tagName : 'section' - model : BaseModel - - /** - * Method-name called by `onReturnKeypress` when used as an event-handler. - * @type String - */ - callOnReturnKeypress: null - - - /** - * Parent view of this view. - * @type BaseView - */ - parent : null - - /** - * Array of [view, selector]-pairs. - * @type Array<[BaseView, String]> - */ - subviews : [] - - /** - * Whether this view has been added to the DOM. - * @type Boolean - */ - isAttached: false - - - - constructor : function BaseView - @__class__ = @constructor - @__superclass__ = @..__super__.constructor - @waitingOn = 0 - @subviews = new ViewList - @onReturnKeypress = _.debounce @onReturnKeypress.bind(this), 50 - Backbone.View ... - @trigger 'create', this - - initialize: -> - @__apply_bind__() - - @setModel @model - @build() - @$el.on 'form submit', -> it.preventDefault() - - setModel: (model) -> - if @model - @model.off 'change', @render, this - @model.off 'destroy', @remove, this - delete @model.view - data = @$el.data() - delete data.model - delete data.view - if @model = model - @model.view = this - @$el.data { @model, view:this } - @model.on 'change', @render, this - @model.on 'destroy', @remove, this - @trigger 'change:model', this, model - model - - - - ### Subviews {{{ - - setParent: (parent) -> - [old_parent, @parent] = [@parent, parent] - @trigger 'parent', this, parent, old_parent - this - - unsetParent: -> - [old_parent, @parent] = [@parent, null] - @trigger 'unparent', this, old_parent - this - - - addSubview: (view) -> - @removeSubview view - @subviews.push view - view.setParent this - view - - removeSubview: (view) -> - if @hasSubview view - view.remove() - @subviews.remove view - view.unsetParent() - view - - hasSubview: (view) -> - @subviews.contains view - - invokeSubviews: -> - @subviews.invoke ...arguments - - removeAllSubviews: -> - @subviews.forEach @removeSubview, this - # @subviews = new ViewList - this - - - - ### }}} - ### DOM Utilities {{{ - - attach: (el) -> - # @undelegateEvents() - @$el.appendTo el - # only trigger the event the first time - return this if @isAttached - @isAttached = true - _.delay do - ~> # have to let DOM settle to ensure elements can be found - @delegateEvents() - @trigger 'attach', this - 50 - this - - remove : -> - # @undelegateEvents() - @$el.remove() - return this unless @isAttached - @isAttached = false - @trigger 'unattach', this - this - - clear : -> - @remove() - @model.destroy() - @trigger 'clear', this - this - - hide : -> @$el.hide(); @trigger('hide', this); this - show : -> @$el.show(); @trigger('show', this); this - - /** - * Attach each subview to its bind-point. - * @returns {this} - */ - attachSubviews: -> - bps = @getOwnSubviewBindPoints() - if @subviews.length and not bps.length - console.warn "#this.attachSubviews(): no subview bind-points found!" - return this - for view of @subviews - if bp = @findSubviewBindPoint view, bps - view.attach bp - else - console.warn "#this.attachSubviews(): Unable to find bind-point for #view!" - this - - /** - * Finds all subview bind-points under this view's element, but not under - * the view element of any subview. - * @returns {jQuery|undefined} - */ - getOwnSubviewBindPoints: -> - @$ '[data-subview]' .not @$ '[data-subview] [data-subview]' - - /** - * Find the matching subview bind-point for the given view. - */ - findSubviewBindPoint: (view, bind_points) -> - bind_points or= @getOwnSubviewBindPoints() - - # check if any bindpoint specifies this subview by id - if view.id - bp = bind_points.filter "[data-subview$=':#{view.id}']" - return bp.eq 0 if bp.length - - # Find all elements that specify this type as the subview type - bp = bind_points.filter "[data-subview='#{view.getClassName()}']" - return bp.eq 0 if bp.length - - - ### }}} - ### Rendering Chain {{{ - - toTemplateLocals: -> - @model.toJSON() - - $template: -> - $ @template { _, op, @model, view:this, ...@toTemplateLocals() } - - build: -> - return this unless @template - outer = @$template() - @$el.html outer.html() - .attr do - id : outer.attr 'id' - class : outer.attr 'class' - @attachSubviews() - @isBuilt = true - this - - render: -> - @wait() - if @isBuilt - @update() - else - @build() - @renderSubviews() - @trigger 'render', this - @unwait() - this - - renderSubviews: -> - @attachSubviews() - @subviews.invoke 'render' - this - - update: -> - new DataBinding this .update locals = @toTemplateLocals() - @trigger 'update', this, locals - this - - - /* * * * Events * * * */ - - bubbleEventDown: (evt) -> - @invokeSubviews 'trigger', ...arguments - this - - redispatch: (evt, ...args) -> - @trigger evt, this, ...args - this - - onlyOnReturn: (fn, ...args) -> - fn = _.debounce fn.bind(this), 50 - (evt) ~> fn.apply this, args if evt.keyCode is 13 - - /** - * Call a delegate on keypress == the return key. - * @returns {Function} Keypress event handler. - */ - onReturnKeypress: (evt) -> - fn = this[@callOnReturnKeypress] if @callOnReturnKeypress - fn.call this if fn and evt.keyCode is 13 - - toString : -> - "#{@getClassName()}(model=#{@model})" - - -# Proxy model methods -<[ get set unset toJSON toKV toURL ]> - .forEach (methodname) -> - BaseView::[methodname] = -> @model[methodname].apply @model, arguments - -# }}} - - - -class exports.ViewList extends Array - - (views=[]) -> - super ... - - extend: (views) -> - _.each views, ~> @push it - this - - findByModel: (model) -> - @find -> it.model is model - - toString: -> - contents = if @length then "\"#{@join '","'}\"" else '' - "ViewList[#{@length}](#contents)" - - -<[ each contains invoke pluck find remove compact flatten without union intersection difference unique uniq ]> - .forEach (methodname) -> - ViewList::[methodname] = -> _[methodname].call _, this, ...arguments - - diff --git a/lib/base/base.co b/lib/base/base.co deleted file mode 100644 index 5d3f699..0000000 --- a/lib/base/base.co +++ /dev/null @@ -1,74 +0,0 @@ -{EventEmitter} = require 'events' -EventEmitter::off = EventEmitter::removeListener -EventEmitter::trigger = EventEmitter::emit - -{ _, op -} = require 'kraken/util' - - - -/** - * @class Eventful base class. - * @extends EventEmitter - */ -class Base extends EventEmitter - - /** - * After the super chain has exhausted (but not necessarily at the end - * of init -- it depends on when you super()), Base will publish a 'new' - * event on the instance's class, allowing anyone to subscribe to - * notifications about new objects. - * @constructor - */ - -> - @__class__ = @.. - @__superclass__ = @..superclass - @__apply_bind__() - super() - @__class__.emit 'new', this - - - ### Auto-Bound methods - - /** - * A list of method-names to bind on `initialize`; set this on a subclass to override. - * @type Array - */ - __bind__ : [] - - /** - * Applies the contents of `__bind__`. - */ - __apply_bind__: -> - names = _ @pluckSuperAndSelf '__bind__' .chain().flatten().compact().unique().value() - _.bindAll this, ...names if names.length - - - - getClassName: -> - "#{@..name or @..displayName}" - - toString: -> - "#{@getClassName()}()" - - - - ### Class Methods - - @extended = (Subclass) -> - # copy over all class methods, including this - for own k, v in this - Subclass[k] = v if typeof v is 'function' - Subclass.__super__ = @:: - Subclass - - - - -for k of <[ getSuperClasses pluckSuper pluckSuperAndSelf ]> - Base[k] = Base::[k] = _.methodize _[k] - -Base import EventEmitter:: - - -module.exports = Base diff --git a/lib/base/cascading-model.co b/lib/base/cascading-model.co deleted file mode 100644 index 7582621..0000000 --- a/lib/base/cascading-model.co +++ /dev/null @@ -1,57 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseModel, BaseList, -} = require 'kraken/base/base-model' - -Cascade = require 'kraken/util/cascade' - - - -/** - * @class A model that implements cascading lookups for its attributes. - */ -CascadingModel = exports.CascadingModel = BaseModel.extend do # {{{ - /** - * The lookup cascade. - * @type Cascade - */ - cascade : null - - - constructor: function CascadingModel (attributes={}, opts) - @cascade = new Cascade attributes - BaseModel.call this, attributes, opts - - initialize: -> - BaseModel::initialize ... - - - /** - * Recursively look up a (potenitally nested) attribute in the lookup chain. - * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys). - * @returns {*} - */ - get: (key) -> - @cascade.get key - - - toJSON: (opts={}) -> - opts = {-collapseCascade, ...opts} - if opts.collapseCascade - @cascade.collapse() - else - BaseModel::toJSON ... - - - -# Proxy Cascade methods -<[ - addLookup removeLookup popLookup shiftLookup unshiftLookup - isOwnProperty isOwnValue isInheritedValue isModifiedValue -]>.forEach (methodname) -> - CascadingModel::[methodname] = -> @cascade[methodname].apply @cascade, arguments - -# }}} - - - diff --git a/lib/base/data-binding.co b/lib/base/data-binding.co deleted file mode 100644 index 4b2fa58..0000000 --- a/lib/base/data-binding.co +++ /dev/null @@ -1,67 +0,0 @@ -Backbone = require 'backbone' - -{ _, op, -} = require 'kraken/util' - - -class exports.DataBinding - - data : null - context : null - el : null - $el : null - bindPoints: null - - - - (el, @context=el) -> - if el instanceof Backbone.View - el = el.$el - @$el = $ el - @el = @$el.get 0 - - # Find all bind-points under this element, but not under a subview - @bindPoints = @$ '[data-bind], [name]' .not @$('[data-subview]').find('[data-bind], [name]') - - $: (sel) -> - @$el.find sel - - serialize: -> - it - - update: (@data) -> - for key, val in _.collapseObject(@data) - @updateBinding key, val - this - - updateBinding: (key, val) -> - # if val and _.isPlainObject val - # for k, v in val - # @updateBinding "#key.#k", v - # return this - - if bp = @findDataBindPoint key - if _.isFunction val - val.call @context, val, key, bp, @data - else if bp.is 'input:checkbox' - bp.attr 'checked', !!val - else if bp.is 'input, textarea' - bp.val @serialize val - else - if op.toBool bp.data 'data-bind-escape' - bp.text @serialize val - else - bp.html @serialize val - else - false and console.warn "#this.updateBinding(): Unable to find data bind-point for #key=#val!" - this - - findDataBindPoint: (key) -> - bp = @bindPoints.filter "[name='#key'], [data-bind='#key']" - return bp.eq(0) if bp.length - - - - - - diff --git a/lib/base/index.co b/lib/base/index.co deleted file mode 100644 index 17b6726..0000000 --- a/lib/base/index.co +++ /dev/null @@ -1,9 +0,0 @@ -exports.Base = require 'kraken/base/base' -mixins = require 'kraken/base/base-mixin' -models = require 'kraken/base/base-model' -views = require 'kraken/base/base-view' -cache = require 'kraken/base/model-cache' -cascading = require 'kraken/base/cascading-model' -data_binding = require 'kraken/base/data-binding' -exports import mixins import models import views \ - import cache import cascading import data_binding diff --git a/lib/base/model-cache.co b/lib/base/model-cache.co deleted file mode 100644 index f636a7d..0000000 --- a/lib/base/model-cache.co +++ /dev/null @@ -1,196 +0,0 @@ -_ = require 'underscore' -Seq = require 'seq' - -{ReadyEmitter} = require 'kraken/util/event' - - -# TODO: Bubble events to decorated emitters -# TODO: Automatically create a cache for any class that extends BaseModel -/** - * @class Caches models and provides static lookups by ID. - */ -class exports.ModelCache extends ReadyEmitter - /** - * @see ReadyEmitter#readyEventName - * @private - * @constant - * @type String - */ - readyEventName : 'cache-ready' - - /** - * Default options. - * @private - * @constant - * @type Object - */ - DEFAULT_OPTIONS: - ready : true - cache : null - create : null - ModelType : null - - /** - * @private - * @type Object - */ - options : null - - /** - * Type we're caching (presumably extending `Backbone.Model`), used to create new - * instances unless a `create` function was provided in options. - * @private - * @type Class - */ - ModelType : null - - /** - * Collection holding the cached Models. - * @private - * @type Backbone.Collection - */ - cache : null - - - - /** - * @constructor - * @param {Class} [ModelType] Type of cached object (presumably extending - * `Backbone.Model`), used to create new instances unless `options.create` - * is provided. - * @param {Object} [options] Options: - * @param {Boolean} [options.ready=true] Starting `ready` state. If false, - * the cache will queue lookup calls until `triggerReady()` is called. - * @param {Class} [options.cache=new Backbone.Collection] - * The backing data-structure for the cache. If omitted, we'll use a new - * `Backbone.Collection`, but really, anything with a `get(id)` method for - * model lookup will work here. - * @param {Function} [options.create] A function called when a new Model - * object is needed, being passed the new model ID. - * @param {Class} [options.ModelType] Type of cached object - * (presumably extending `Backbone.Model`), used to create new instances - * unless `options.create` is provided. - */ - (ModelType, options) -> - unless _.isFunction ModelType - [options, ModelType] = [ModelType or {}, null] - @options = {...@DEFAULT_OPTIONS, ...options} - - @cache = @options.cache or new Backbone.Collection - - @ModelType = ModelType or @options.ModelType - @createModel = that if @options.create - - @ready = !!@options.ready - @decorate @ModelType if @ModelType - - - /** - * Called when a new Model object is needed, being passed the new model ID. - * Uses the supplied `ModelType`; overriden by `options.create` if provided. - * - * @param {String} id The model ID to create. - * @returns {Model} Created model. - */ - createModel: (id) -> - new @ModelType {id} - - /** - * Registers a model with the cache. If a model by this ID already exists - * in the cache, it will be removed and this one will take its place. - * - * Fires an `add` event. - * - * @param {Model} model The model. - * @returns {Model} The model. - */ - register: (model) -> - # console.log "ModelCache(#{@CACHE}).register(#{model.id or model.get('id')})", model - if @cache.contains model - @cache.remove model, {+silent} - @cache.add model - @trigger 'add', this, model - model - - /** - * Synchronously check if a model is in the cache, returning it if so. - * - * @param {String} id The model ID to get. - * @returns {Model} - */ - get: (id) -> - @cache.get id - - /** - * Asynchronously look up any number of models, requesting them from the - * server if not already known to the cache. - * - * @param {String|Array} ids List of model IDs to lookup. - * @param {Function} cb Callback of the form `(err, models)`, - * where `err` will be null on success and `models` will be an Array - * of model objects. - * @param {Object} [cxt=this] Callback context. - * @returns {this} - */ - lookupAll: (ids, cb, cxt=this) -> - ids = [ids] unless _.isArray ids - # console.log "ModelCache(#{@cache}).lookup([#ids], #{typeof cb})" - - unless @ready - @on 'cache-ready', ~> - @off 'cache-ready', arguments.callee - @lookupAll ids, cb, cxt - return this - - Seq ids - .parMap_ (next, id) ~> - return next.ok(that) if @cache.get id - @register @createModel id - .on 'ready', -> next.ok it - .load() - .unflatten() - .seq (models) -> - cb.call cxt, null, models - .catch (err) -> - cb.call cxt, err - this - - /** - * Looks up a model, requesting it from the server if it is not already - * known to the cache. - * - * @param {String|Array} id Model ID to lookup. - * @param {Function} cb Callback of the form `(err, model)`, - * where `err` will be null on success and `model` will be the - * model object. - * @param {Object} [cxt=this] Callback context. - * @returns {this} - */ - lookup: (id, cb, cxt=this) -> - @lookupAll [id], (err, models) -> - if err then cb.call cxt, err - else cb.call cxt, null, models[0] - - /** - * Decorate an object with the cache methods: - * - register - * - get - * - lookup - * - lookupAll - * - * This is automatically called on `ModelType` if supplied. - * - * @param {Object} obj Object to decorate. - * @returns {obj} The supplied object. - */ - decorate: (obj) -> - obj.__cache__ = this - # Bind the ModelCache methods to the class - for m of <[ register get lookup lookupAll ]> - obj[m] = @[m].bind this - obj - - toString: -> - "#{@..displayName or @..name}(cache=#{@cache})" - - diff --git a/lib/base/scaffold/index.co b/lib/base/scaffold/index.co deleted file mode 100644 index 6524ae6..0000000 --- a/lib/base/scaffold/index.co +++ /dev/null @@ -1,3 +0,0 @@ -models = require 'kraken/base/scaffold/scaffold-model' -views = require 'kraken/base/scaffold/scaffold-view' -exports import models import views diff --git a/lib/base/scaffold/scaffold-model.co b/lib/base/scaffold/scaffold-model.co deleted file mode 100644 index 2800782..0000000 --- a/lib/base/scaffold/scaffold-model.co +++ /dev/null @@ -1,105 +0,0 @@ -_ = require 'kraken/util/underscore' -op = require 'kraken/util/op' -{ BaseModel, BaseList, -} = require 'kraken/base' - - - -### Scaffold Models - -Field = exports.Field = BaseModel.extend do # {{{ - valueAttribute : 'value' - - defaults: -> - name : '' - type : 'String' - default : null - desc : '' - include : 'diff' - tags : [] - examples : [] - - - - constructor: function Field - BaseModel ... - - initialize: -> - _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse')) - @set 'id', @id = _.camelize @get 'name' - @set 'value', @get('default'), {+silent} if not @has 'value' - Field.__super__.initialize ... - - - - - - - /* * * Value Accessors * * */ - - getValue: (def) -> - @getParser() @get @valueAttribute, def - - setValue: (v, options) -> - def = @get 'default' - if not v and def == null - val = null - else - val = @getParser()(v) - @set @valueAttribute, val, options - - clearValue: -> - @set @valueAttribute, @get 'default' - - isDefault: -> - @get(@valueAttribute) is @get 'default' - - - /* * * Serializers * * */ - - serializeValue: -> - @serialize @getValue() - - toJSON: -> - {id:@id} import do - _.clone(@attributes) import { value:@getValue(), def:@get 'default' } - - toKVPairs: -> - { "#{@id}":@serializeValue() } - - toString: -> "(#{@id}: #{@serializeValue()})" - -# }}} - - -FieldList = exports.FieldList = BaseList.extend do # {{{ - model : Field - - constructor: function FieldList - BaseList ... - - - /** - * Collects a map of fields to their values, excluding those set to `null` or their default. - * @returns {Object} - */ - values: (opts={}) -> - opts = {+keepDefaults, -serialize} import opts - _.synthesize do - if opts.keepDefaults then @models else @models.filter -> not it.isDefault() - -> [ it.get('name'), if opts.serialize then it.serializeValue() else it.getValue() ] - - toJSON: -> - @values {+keepDefaults, -serialize} - - toKVPairs: -> - _.collapseObject @values {+keepDefaults, +serialize} - - toKV: (item_delim='&', kv_delim='=') -> - _.toKV @toKVPairs(), item_delim, kv_delim - - toURL: (item_delim='&', kv_delim='=') -> - "?#{@toKV ...}" - -# }}} - diff --git a/lib/base/scaffold/scaffold-view.co b/lib/base/scaffold/scaffold-view.co deleted file mode 100644 index b5299bc..0000000 --- a/lib/base/scaffold/scaffold-view.co +++ /dev/null @@ -1,125 +0,0 @@ -_ = require 'kraken/util/underscore' -op = require 'kraken/util/op' -{ BaseView, -} = require 'kraken/base' -{ Field, FieldList, -} = require 'kraken/base/scaffold/scaffold-model' - - -FieldView = exports.FieldView = BaseView.extend do # {{{ - tagName : 'div' - className : 'field' - - type : 'string' - - events : - 'blur .value' : 'onChange' - 'submit .value' : 'onChange' - - - constructor: function FieldView - BaseView ... - - initialize: -> - # console.log "#this.initialize!" - BaseView::initialize ... - @type = @model.get 'type' .toLowerCase() or 'string' - - onChange: -> - if @type is 'boolean' - val = !! @$('.value').attr('checked') - else - val = @model.getParser() @$('.value').val() - - current = @model.getValue() - return if _.isEqual val, current - # console.log "#this.onChange( #current -> #val )" - @model.setValue val, {+silent} - @trigger 'change', this - - toTemplateLocals: -> - json = FieldView.__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)) - json - - /** - * A ghetto default template, typically overridden by superclass. - */ - template: (locals) -> - $ """ - - - """ - - render: -> - return @remove() if @model.get 'ignore' - FieldView.__super__.render ... - -# }}} - - -# There are several special options that, if passed, will be attached directly to the view: -# model, collection, el, id, className, tagName, attributes - -Scaffold = exports.Scaffold = BaseView.extend do # {{{ - __bind__ : <[ addField resetFields ]> - tagName : 'form' - className : 'scaffold' - - collectionType : FieldList - subviewType : FieldView - - - - constructor: function Scaffold - BaseView ... - - initialize: -> - CollectionType = @collectionType - @model = (@collection or= new CollectionType) - BaseView::initialize ... - - @collection.on 'add', @addField, this - @collection.on 'reset', @resetFields, this - - - - addField: (field) -> - @removeSubview field.view if field.view - - # avoid duplicating event propagation - field.off 'change:value', @onChange, this - - # propagate value-change events as key-value change events - field.on 'change:value', @onChange, this - - SubviewType = @subviewType - view = @addSubview new SubviewType model:field - view.on 'change', @onChange.bind(this, field) - - @render() - view - - resetFields: -> - @removeAllSubviews() - @collection.each @addField - this - - onChange: (field) -> - key = field.get 'name' - value = field.getValue() - @trigger "change:#key", this, value, key, field - @trigger "change", this, value, key, field - this - - - -# Proxy collection methods -<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]> - .forEach (methodname) -> - Scaffold::[methodname] = -> @collection[methodname].apply @collection, arguments - -# }}} - diff --git a/lib/chart/chart-type.co b/lib/chart/chart-type.co deleted file mode 100644 index 2840b9d..0000000 --- a/lib/chart/chart-type.co +++ /dev/null @@ -1,425 +0,0 @@ -moment = require 'moment' -Backbone = require 'backbone' - -{ _, op, -} = require 'kraken/util' -{ ReadyEmitter, -} = require 'kraken/util/event' -{ Parsers, ParserMixin, -} = require 'kraken/util/parser' - - - -/** - * Map of known libraries by name. - * @type Object - */ -KNOWN_CHART_TYPES = exports.KNOWN_CHART_TYPES = {} - - -/** - * @class Abstraction of a chart-type or charting library, encapsulating its - * logic and options. In addition, a `ChartType` also mediates the - * transformation of the domain-specific data types (the model and its view) - * with its specific needs. - * - * `ChartType`s mix in `ParserMixin`: when implementing a `ChartType`, you can - * add or supplement parsers merely by subclassing and overriding the - * corresponding `parseXXX` method (such as `parseArray` or `parseDate`). - * - * @extends EventEmitter - * @borrows ParserMixin - */ -class exports.ChartType extends ReadyEmitter - - ### Class Methods - - /** - * Register a new chart type. - */ - @register = (Subclass) -> - # console.log "ChartType.register(#Subclass)" - KNOWN_CHART_TYPES[ Subclass::typeName ] = Subclass - - /** - * Look up a `ChartType` by `typeName`. - */ - @lookup = (name) -> - name = name.get('chartType') if name instanceof Backbone.Model - KNOWN_CHART_TYPES[name] - - /** - * Look up a chart type by name, returning a new instance - * with the given model (and, optionally, view). - * @returns {ChartType} - */ - @create = (model, view) -> - # console.log "ChartType.create(#model) ->", model - return null unless Type = @lookup model - new Type model, view - - - ### Class Properties - /* - * These are "class properties": each is set on the prototype at the class-level, - * and the reference is therefore shared by all instances. It is expected you - * will not modify this on the instance-level. - */ - - /** - * URL for the Chart Spec JSON. Loaded once, the first time an instance of - * that class is created. - * @type String - * @readonly - */ - SPEC_URL : null - - /** - * Chart-type name. - * @type String - * @readonly - */ - typeName: null - - /** - * Map of option name to ChartOption objects. - * @type { name:ChartOption, ... } - * @readonly - */ - options : null - - /** - * Ordered ChartOption objects. - * - * This is a "class-property": it is set on the prototype at the class-level, - * and the reference is shared by all instances. It is expected you will not - * modify that instance. - * - * @type ChartOption[] - * @readonly - */ - options_ordered : null - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - - /** - * Whether the ChartType has loaded all its data and is ready. - * @type Boolean - */ - ready: false - - - - ### Instance properties - - /** - * Model to be rendered as a chart. - * @type Backbone.Model - */ - model : null - - /** - * View to render the chart into. - * @type Backbone.View - */ - view : null - - /** - * Last chart rendered by this ChartType. - * @private - */ - chart: null - - - - - /** - * @constructor - */ - (@model, @view) -> - @roles or= {} - _.bindAll this, ...@__bind__ # TODO: roll up MRO - @loadSpec() unless @ready - - - # Builder Pattern - withModel : (@model) -> this - withView : (@view) -> this - - - /** - * Load the corresponding chart specification, which includes - * info about valid options, along with their types and defaults. - */ - loadSpec: -> - return this if @ready - proto = @constructor:: - jQuery.ajax do - url : @SPEC_URL - dataType : 'json' - success : (spec) ~> - proto.spec = spec - proto.options_ordered = spec - proto.options = _.synthesize spec, -> [it.name, it] - proto.ready = true - @triggerReady() - error: ~> console.error "Error loading #{@typeName} spec! #it" - this - - - /** - * @returns {ChartOption} Get an option's spec by name. - */ - getOption: (name, def) -> - @options[name] or def - - - /** - * @returns {Object} An object, mapping from option.name to the - * result of the supplied function. - */ - map: (fn, context=this) -> - _.synthesize @options, ~> [it.name, fn.call(context, it, it.name, this)] - - - /** - * @param {String} attr Attribute to look up on each options object. - * @returns {Object} Map from name to the value found at the given attr. - */ - pluck: (attr) -> - @map -> it[attr] - - - /** - * @returns {Boolean} Whether the supplied value is the same as - * the default value for the given key. - */ - isDefault: (name, value) -> - _.isEqual @getOption(name).default, value - - - ### }}} - ### Parsers & Serialization {{{ - - /** - * When implementing a ChartType, you can add or override parsers - * merely by subclassing. - * @borrows ParserMixin - */ - ParserMixin.mix this - - /** - * @returns {Function} Parser for the given option name. - */ - getParserFor: (name) -> - @getParser @getOption(name).type - - /** - * Parses a single serialized option value into its proper type. - * - * @param {String} name Option-name of the value being parsed. - * @param {String} value Value to parse. - * @returns {*} Parsed value. - */ - parseOption: (name, value) -> - @getParserFor(name)(value) - - /** - * Parses options using `parseOption(name, value)`. - * - * @param {Object} options Options to parse. - * @returns {Object} Parsed options. - */ - parseOptions: (options) -> - out = {} - for k, v in options - out[k] = @parseOption k, v - out - - - /** - * Serializes option-value to a String. - * - * @param {*} v Value to serialize. - * @param {String} k Option-name of the given value. - * @returns {String} The serialized value - */ - serialize: (v, k) -> - # if v!? - # v = '' - if _.isBoolean v - v = Number v - else if _.isObject v - v = JSON.stringify v - String v - - - ### }}} - ### Formatters {{{ - - /** - * 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. - * @param {Boolean} [abbrev=true] Expand number suffixes if false. - * @returns {Object} Formatted number parts. - */ - numberFormatter: (n, digits=2, abbrev=true) -> - suffixes = do - if abbrev - [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] - else - [['Billion', 1000000000], ['Million', 1000000], ['', NaN]] - - for [suffix, d] of suffixes - 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, toString: -> - "#{@whole}#{@fraction}#{if abbrev then '' else ' '}#{@suffix}" - } - - - ### }}} - ### Rendering {{{ - - /** - * Finds the element in the view which plays the given role in the chart. - * Canonically, all charts have a "viewport" element. Other roles might - * include a "legend" element, or several "axis" elements. - * - * Default implementation looks up a selector in the `roles` hash, and if - * found, queries the view for matching children. - * - * @param {String} role Name of the role to look up. - * @returns {jQuery|null} $-wrapped DOM element. - */ - getElementsForRole: (role) -> - return null unless @view - if @roles[role] - @view.$ that - else - null - - - /** - * Transform/extract the data for this chart from the model. Default - * implementation calls `model.getData()`. - * - * @returns {*} Data object for the chart. - */ - getData: -> - @model.getData() - - - /** - * Map from option-name to default value. Note that this reference will be - * modified by `.render()`. - * - * @returns {Object} Default options. - */ - getDefaultOptions: -> - @pluck 'default' - - - - - /** - * Resizes the HTML viewport. Override to disable, etc. - */ - resizeViewport: -> - size = @determineSize() - @getElementsForRole 'viewport' .css size - size - - /** - * Determines chart viewport size. - * @return { width, height } - */ - determineSize: -> - modelW = width = @model.get 'width' - modelH = height = @model.get 'height' - return { width, height } unless @view.ready and width and height - - viewport = @getElementsForRole 'viewport' - - if width is 'auto' - Width = viewport.innerWidth() or 300 - width ?= modelW - - if height is 'auto' - height = viewport.innerHeight() or 320 - height ?= modelH - - { width, height } - - - /** - * Transforms domain data and applies it to the chart library to - * render or update the corresponding chart. - * - * @returns {Chart} - */ - render: -> - 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 - - - /** - * Transforms the domain objects into a hash of derived values using - * chart-type-specific keys. - * - * Default implementation returns `model.getOptions()`. - * - * @returns {Object} The derived data. - */ - transform: (model, view) -> - @model.getOptions() - - - /** - * Called to render the chart. - * - * @abstract - * @returns {Chart} - */ - renderChart: (data, viewport, options, lastChart) -> - ... - - - ### }}} - diff --git a/lib/chart/index.co b/lib/chart/index.co deleted file mode 100644 index c5fd1e2..0000000 --- a/lib/chart/index.co +++ /dev/null @@ -1,8 +0,0 @@ -chart_type = require 'kraken/chart/chart-type' -chart_option = require 'kraken/chart/option' -dygraphs = require 'kraken/chart/type/dygraphs' -d3_chart = require 'kraken/chart/type/d3-chart' -d3_elements = require 'kraken/chart/type/d3' - -exports import chart_type import chart_option \ - import dygraphs import d3_chart import d3_elements diff --git a/lib/chart/option/chart-option-model.co b/lib/chart/option/chart-option-model.co deleted file mode 100644 index 2a3c37a..0000000 --- a/lib/chart/option/chart-option-model.co +++ /dev/null @@ -1,218 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ Parsers, ParserMixin, ParsingModel, ParsingView, -} = require 'kraken/util/parser' -{ BaseModel, BaseList, -} = require 'kraken/base' - - -/** - * @class A set of tags. - */ -class exports.TagSet extends Array - tags : {} - - (values=[]) -> - @tags = {} - @add values if values?.length - - has: (tag) -> - @tags[tag]? - - get: (tag) -> - return -1 unless tag - unless @tags[tag]? - @tags[tag] = @length - @push tag - @tags[tag] - - update: (tags) -> - is_single = typeof tags is 'string' - tags = [tags] if is_single - indices = ( for tag of tags then @get tag ) - if is_single then indices[0] else indices - - toString: -> "TagSet(length=#{@length}, values=[\"#{@join '", "'}\"])" - - - -/** - * @namespace All known tags, for mapping consistently onto colors. - */ -KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet() - - - -/** - * @class Field with chart-option-specific handling for validation, parsing, tags, etc. - */ -ChartOption = exports.ChartOption = ParsingModel.extend do # {{{ - IGNORED_TAGS : <[ callback deprecated debugging ]> - valueAttribute : 'value' - - defaults: -> - name : '' - type : 'String' - default : null - desc : '' - include : 'diff' - tags : [] - examples : [] - - - - constructor: function ChartOption - ParsingModel ... - - initialize : -> - # console.log "#this.initialize!" - - # Bind all the `parseXXX()` methods so they can be passed about independent from the class - _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse')) - - ChartOption.__super__.initialize ... - @set 'id', @id = _.camelize @get 'name' - @set 'value', @get('default'), {+silent} if not @has 'value' - - - # Notify Tag indexer of category when created, to ensure all category-tags - # get indices with colors :P - KNOWN_TAGS.update @getCategory() - - # Ignore functions/callbacks and, ahem, hidden tags. - type = @get 'type' .toLowerCase() or '' - tags = @get('tags') or [] - if _.str.include(type, 'function') or _.intersection(tags, @IGNORED_TAGS).length - @set 'ignore', true - - - - ### Tag Handling - - # Wrapper to ensure @set('tags') is called, as tags.push() - # will not trigger the 'changed:tags' event. - addTag: (tag) -> - return this unless tag - tags = @get('tags') or [] - tags.push tag - @set 'tags', tags - this - - # Wrapper to ensure @set('tags') is called, as tags.push() - # will not trigger the 'changed:tags' event. - removeTag: (tag) -> - return this unless tag - tags = @get('tags') or [] - _.remove tags, tag - @set 'tags', tags - this - - # Keep tag list up to date - onTagUpdate: -> - KNOWN_TAGS.update @get 'tags' - this - - getTagIndex: (tag) -> - KNOWN_TAGS.get tag - - # A field's category is its first tag. - getCategory: -> - tags = (@get('tags') or [])[0] - - getCategoryIndex: -> - @getTagIndex @getCategory() - - - - /* * * Value Accessors * * */ - - getValue: (def) -> - @getParser() @get @valueAttribute, def - - setValue: (v, options) -> - def = @get 'default' - if not v and def == null - val = null - else - val = @getParser()(v) - @set @valueAttribute, val, options - - clearValue: -> - @set @valueAttribute, @get 'default' - - isDefault: -> - @get(@valueAttribute) is @get 'default' - - - - /* * * Serialization * * */ - - /** - * Override to default `type` to the model attribute of the same name. - * @returns {Function} Parser for the given type. - */ - getParser: (type) -> - type or= @get('type') or 'String' - ChartOption.__super__.getParser.call this, type - - serializeValue: -> - @serialize @getValue() - - toJSON: -> - {id:@id} import do - _.clone(@attributes) import { value:@getValue(), def:@get 'default' } - - toKVPairs: -> - { "#{@id}":@serializeValue() } - - toString: -> "(#{@id}: #{@serializeValue()})" - -# }}} - - - -/** - * @class List of ChartOption fields. - */ -ChartOptionList = exports.ChartOptionList = BaseList.extend do # {{{ - model : ChartOption - - - constructor: function ChartOptionList - BaseList ... - - - /** - * Collects a map of fields to their values, excluding those set to `null` or their default. - * - * @param {Object} [opts={}] Options: - * @param {Boolean} [opts.keepDefaults=true] If false, exclude pairs that - * haven't changed from their default value. - * @param {Boolean} [opts.serialize=false] If true, replace each value - * with its String version by calling `value.serializeValue()`. - * @returns {Object} Map of fields to their values. - */ - values: (opts={}) -> - opts = {+keepDefaults, -serialize} import opts - _.synthesize do - if opts.keepDefaults then @models else @models.filter -> not it.isDefault() - -> [ it.get('name'), if opts.serialize then it.serializeValue() else it.getValue() ] - - toJSON: -> - @values {+keepDefaults, -serialize} - - /** - * Override to omit defaults from URL. - */ - toKVPairs: -> - _.collapseObject @values {-keepDefaults, +serialize} - - toKV: (item_delim='&', kv_delim='=') -> - _.toKV @toKVPairs(), item_delim, kv_delim - - toURL: (item_delim='&', kv_delim='=') -> - "?#{@toKV ...}" - - -# }}} - diff --git a/lib/chart/option/chart-option-view.co b/lib/chart/option/chart-option-view.co deleted file mode 100644 index 1b004e1..0000000 --- a/lib/chart/option/chart-option-view.co +++ /dev/null @@ -1,270 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseView, -} = require 'kraken/base' -{ ChartOption, ChartOptionList, -} = require 'kraken/chart/option/chart-option-model' - -DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms - - -/** - * @class View for a single configurable option in a chart type. - */ -ChartOptionView = exports.ChartOptionView = BaseView.extend do # {{{ - tagName : 'section' - className : 'chart-option field' - template : require 'kraken/template/chart/chart-option' - - type : 'string' - isCollapsed : true - - events : - 'blur .value' : 'onChange' - 'click input[type="checkbox"].value' : 'onChange' - 'submit .value' : 'onChange' - 'click .close' : 'toggleCollapsed' - 'click h3' : 'toggleCollapsed' - 'click .collapsed' : 'onClick' - - - - constructor: function ChartOptionView - BaseView ... - - initialize: -> - ChartOptionView.__super__.initialize ... - @type = @model.get 'type' .toLowerCase() or 'string' - - - /* * * * Rendering * * * */ - - toTemplateLocals: -> - json = ChartOptionView.__super__.toTemplateLocals ... - json.id or= _.camelize json.name - json.value ?= '' - v = json.value - json.value = JSON.stringify(v) if v and ( _.isArray(v) or _.isPlainObject(v) ) - json - - /** - * Override to annotate with collapsed state and to kill off ignored options - * so they do not contribute their values when looking at form updates. - */ - render: -> - return @remove() if @model.get 'ignore' - ChartOptionView.__super__.render ... - @$el.addClass 'collapsed' if @isCollapsed - this - - - - /* * * * Option Collapsing * * * */ - - /** - * Sets the state of `isCollapsed` and updates the UI. If the state changed, - * a `'change:collapse`` event will be fired.` - * - * @param {Boolean} [makeCollapsed=true] If true, set state to collapsed. - * @returns {Boolean} Whether the state changed. - */ - collapse: (state=true) -> - state = !! state - @isCollapsed = @$el.hasClass 'collapsed' - - return this if state is @isCollapsed - if state - @$el.addClass 'collapsed' - else - @$el.removeClass 'collapsed' - @isCollapsed = state - @trigger 'change:collapse', this, @isCollapsed - true - - /** - * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event. - * @returns {this} - */ - toggleCollapsed: -> - @collapse not @$el.hasClass 'collapsed' - this - - - - /* * * * Events * * * */ - - /** - * To prevent `toggleCollapsed()` from being called multiple times due to - * overlapping listeners, we're only looking for clicks on the collapsed header. - */ - onClick: (evt) -> - target = $ evt.target - @toggleCollapsed() if @$el.hasClass('collapsed') and not target.hasClass('close') - - /** - * Propagate user input changes to the model, and upward to the parent view. - */ - onChange: -> - if @type is 'boolean' - val = !! @$('.value').attr('checked') - else - val = @model.getParser() @$('.value').val() - - current = @model.getValue() - return if _.isEqual val, current - console.log "#this.onChange( #current -> #val )" - @model.setValue val, {+silent} - @trigger 'change', @model, this - # false - - -# }}} - - - -/** - * @class View for configuring a chart type. - */ -ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend do # {{{ - __bind__ : <[ addField ]> - tagName : 'form' - className : 'chart-options scaffold' - template : require 'kraken/template/chart/chart-scaffold' - - collectionType : ChartOptionList - subviewType : ChartOptionView - - events: - 'click .options-filter-button' : 'onFilterOptions' - 'click .collapse-all-options-button' : 'collapseAll' - 'click .expand-all-options-button' : 'expandAll' - - - - - constructor: function ChartOptionScaffold - BaseView ... - - initialize : -> - @render = _.debounce @render.bind(this), DEBOUNCE_RENDER - CollectionType = @collectionType - @model = (@collection or= new CollectionType) - ChartOptionScaffold.__super__.initialize ... - - @collection.on 'add', @addField, this - @collection.on 'reset', @onReset, this - @on 'render', @onRender, this - - - /** - * Bookkeeping for new ChartOptions, creating it a new subview and subscribing - * to its activity, and then rendering it. - * @returns {ChartOptionView} The Option's new view. - */ - addField: (field) -> - @removeSubview field.view if field.view - - # avoid duplicating event propagation - field.off 'change:value', @onChange, this - - # propagate value-change events as key-value change events - field.on 'change:value', @onChange, this - - SubviewType = @subviewType - @addSubview view = new SubviewType model:field - .on 'change', @onChange.bind(this, field) - .on 'change:collapse', @render, this - - @render() # WTF: hmm. - view - - - /* * * * UI * * * */ - - /** - * Collapse all expanded subviews. - * @returns {false} Returns false so event-dispatchers don't propagate - * the triggering event (usually a click or submit). - */ - collapseAll: -> - _.invoke @subviews, 'collapse', true - false - - /** - * Expand all collapsed subviews. - * @returns {false} Returns false so event-dispatchers don't propagate - * the triggering event (usually a click or submit). - */ - expandAll: -> - _.invoke @subviews, 'collapse', false - false - - /** - * Reflow Isotope post-`render()`. - */ - onRender: -> - # console.log "#this.onRender(ready=#{@ready}) -> .isotope()" - - # The DOM doesn't calculate dimensions of elements that are not visible, - # which makes it impossible for Isotope to do its job. - return unless @$el.is ':visible' - - # Invoke Isotope to re-layout the option elements - @$ '.isotope' .isotope do - # itemPositionDataEnabled : true - itemSelector : '.chart-option.field' - layoutMode : 'masonry' - masonry : { columnWidth:10 } - filter : @getOptionsFilter() - sortBy : 'category' - getSortData : - category: ($el) -> - $el.data 'model' .getCategory() - - /** - * @returns {String} Selector representing the selected set of Option filters. - */ - getOptionsFilter: -> - data = @$ '.options-filter-button.active' .toArray().map -> $ it .data() - sel = data.reduce do - (sel, d) -> sel += if d.filter then that else '' - ':not(.ignore)' - sel - - - - /* * * * Events * * * */ - - /** - * Propagate change events from fields as if they were attribute changes. - * Note: `field` is bound to the handler - */ - onChange: (field) -> - key = field.get 'name' - value = field.getValue() - @trigger "change:#key", this, value, key, field - @trigger "change", this, value, key, field - this - - onReset: -> - # The collection has been reset, assume all subviews are - # invalid and rebuild them. - @removeAllSubviews() - @collection.each @addField - _.defer @render - - onFilterOptions: (evt) -> - evt.preventDefault() - # Defer re-rendering until after we yield for the DOM to do its thang - _.defer @render - - -# Proxy collection methods -<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]> - .forEach (methodname) -> - ChartOptionScaffold::[methodname] = -> @collection[methodname].apply @collection, arguments - - -# }}} - - diff --git a/lib/chart/option/index.co b/lib/chart/option/index.co deleted file mode 100644 index 5cdea4d..0000000 --- a/lib/chart/option/index.co +++ /dev/null @@ -1,4 +0,0 @@ -model = require 'kraken/chart/option/chart-option-model' -view = require 'kraken/chart/option/chart-option-view' - -exports import model import view diff --git a/lib/chart/type/d3-chart.co b/lib/chart/type/d3-chart.co deleted file mode 100644 index 02c2c34..0000000 --- a/lib/chart/type/d3-chart.co +++ /dev/null @@ -1,138 +0,0 @@ -d3 = require 'd3' -ColorBrewer = require 'colorbrewer' - -{ _, op, -} = require 'kraken/util' -{ ChartType, -} = require 'kraken/chart/chart-type' -{ D3ChartElement, -} = require 'kraken/chart/type/d3/d3-chart-element' - - -root = do -> this - - -class exports.D3ChartType extends ChartType - __bind__ : <[ determineSize ]> - SPEC_URL : '/schema/d3/d3-chart.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'd3-chart' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - - - -> super ... - - - getData: -> - @model.dataset.getColumns() - - - transform: -> - dataset = @model.dataset - options = @model.getOptions() import @determineSize() - options import do - colors : dataset.getColors() - labels : dataset.getLabels() - options - - - renderChart: (data, viewport, options, lastChart) -> - ### Starting with http://bost.ocks.org/mike/chart/ - - - # margin convention http://bl.ocks.org/3019563 - margin = {top: 20, right: 20, bottom: 20, left: 20} - width = 760 - margin.left - margin.right - height = 320 - margin.top - margin.bottom - - xScale = d3.time.scale() - yScale = d3.scale.linear() - - dates = data[0] - cols = data.slice(1) - - # Calculate extents using all the data points (but not dates) - # allValues = d3.merge @model.dataset.getDataColumns() - allValues = d3.merge cols - - # Update the x-scale with the extents of the dates. - xScale - .domain d3.extent dates - .range [ 0, width ] - - # Update the y-scale with the extents of the data. - yScale - .domain d3.extent allValues - .range [ height, 0 ] - - - # Hack. Remove svg if it exists. If @model changes, the graph will be redrawn - svg = d3.select viewport.0 .selectAll "svg" - .remove() - # Select the svg element, if it exists. - svg = d3.select viewport.0 .selectAll "svg" - .data [cols] - # ...Otherwise, create the skeletal chart. - enterFrame = svg.enter() - .append "svg" .append "g" - .attr "class", "frame" - - # Update chart dimensions. - svg .attr "width", width + margin.left + margin.right - .attr "height", height + margin.top + margin.bottom - - frame = svg.select "g.frame" - .attr "transform", "translate(#{margin.left},#{margin.top})" - .attr "width", width - .attr "height", height - - - - # x-axis. - # TODO move axis to separate chart-type - enterFrame.append "g" - .attr "class", "x axis time" - - - xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0) - frame.select ".x.axis.time" - .attr "transform", "translate(0,#{yScale.range()[0]})" - .call xAxis - - - metrics = frame.selectAll "metric" - .data @model.dataset.metrics.models - - metrics.enter() - .append("g") - .attr "class", (d) -> - "g metric line "+d.get 'label' - .each (d) -> - # console.log d - # metric defined charttype - chartElement = d.get "chartElement" - # otherwise the graph defined charttype - # FOR NOW take line as default - chartElement ?= 'd3-line' # @model.get "chartElement" - # create d3 chart element and render it - chEl = D3ChartElement.create chartElement - - chEl.renderChartElement d, frame ,xScale, yScale - - metrics.exit().remove() - - svg - - diff --git a/lib/chart/type/d3/d3-bar-chart-type.co b/lib/chart/type/d3/d3-bar-chart-type.co deleted file mode 100644 index 6bab42b..0000000 --- a/lib/chart/type/d3/d3-bar-chart-type.co +++ /dev/null @@ -1,239 +0,0 @@ -d3 = require 'd3' - -{ _, op, -} = require 'kraken/util' -{ ChartType, -} = require 'kraken/chart/chart-type' - -root = do -> this - - -class exports.BarChartType extends ChartType - __bind__ : <[ determineSize ]> - SPEC_URL : '/schema/d3/d3-bar.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'd3-bar' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - - -> super ... - - getData: -> - @model.dataset.getColumns() - - - transform: -> - dataset = @model.dataset - options = @model.getOptions() import @determineSize() - options import do - colors : dataset.getColors() - labels : dataset.getLabels() - options - - - renderChartType: (metric, svgEl ,xScale, yScale) -> - - X = (d, i) -> xScale d[0] - Y = (d, i) -> yScale d[1] - - - ### Render the line path - metricBars = root.metricBars = svgEl.append "g" - .attr "class", "metric bars "+metric.get 'label' - - data = d3.zip metric.getDateColumn(),metric.getData() - - ### Render Bars - barWidth = svgEl.attr('width')/data.length - barHeight = (d) -> svgEl.attr('height')-Y(d) - - metricBars.selectAll "bar" - .data data - .enter().append "rect" - .attr "class", (d, i) -> "metric bar #i" - .attr "x", X - .attr "y", Y - .attr "height", barHeight - .attr "width", -> barWidth - .attr "fill", metric.get 'color' - .attr "stroke", "white" - .style "opacity", "0.4" - .style "z-index", -10 - - - # adding event listeners - chT = this - metricBars.selectAll ".metric.bar" - .on "mouseover", (d, i) -> - - svgEl.append "text" - .attr "class", "mf" - .attr "dx", 50 - .attr "dy", 100 - .style "font-size", "0px" - .transition() - .duration(800) - .text "Uh boy, the target would be: "+chT.numberFormatter(d[1]).toString() - .style "font-size", "25px" - .on "mouseout", (d, i) -> - - svgEl.selectAll ".mf" - .transition() - .duration(300) - .text "BUMMER!!!" - .style "font-size", "0px" - .remove() - - - - svgEl - - renderChart: (data, viewport, options, lastChart) -> - ### Starting with http://bost.ocks.org/mike/chart/ - - margin = {top: 20, right: 20, bottom: 20, left: 20} - width = 760 - height = 320 - xScale = d3.time.scale() - yScale = d3.scale.linear() - - dates = data[0] - cols = data.slice(1) - - # Calculate extents using all the data points (but not dates) - # allValues = d3.merge @model.dataset.getDataColumns() - allValues = d3.merge cols - - - # Update the x-scale with the extents of the dates. - xScale - .domain d3.extent dates - .range [ 0, width - margin.left - margin.right ] - - # Update the y-scale with the extents of the data. - yScale - .domain d3.extent allValues - .range [ height - margin.top - margin.bottom, 0 ] - - # Select the svg element, if it exists. - svg = d3.select viewport.0 .selectAll "svg" - .data [cols] - - # ...Otherwise, create the skeletal chart. - enterFrame = svg.enter() - .append "svg" .append "g" - .attr "class", "frame" - enterFrame.append "g" - .attr "class", "x axis time" - - # Update chart dimensions. - svg .attr "width", width - .attr "height", height - frame = svg.select "g.frame" - .attr "transform", "translate(#{margin.left},#{margin.top})" - - # Update the x-axis. - xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0) - frame.select ".x.axis.time" - .attr "transform", "translate(0,#{yScale.range()[0]})" - .call xAxis - - X = (d, i) -> xScale d[0] - Y = (d, i) -> yScale d[1] - - ### Render Bars - barWidth = svg.attr('width')/dates.length - barHeight = (d) -> svg.attr('height')-Y(d) - - bars = frame.selectAll "g.bars" - .data cols.map -> d3.zip dates, it - bars.enter().append "g" - .attr "class", (col, i) -> "metric bars #i" - bars.exit().remove() - - bars.selectAll ".bar" - .data op.first - .enter().append "rect" - .attr "class", "bar" - .attr "x", X - .attr "y", Y - .attr "height", barHeight - .attr "width", -> barWidth - # TODO grab color from graph spec - .attr "fill", "red" - .attr "stroke", "white" - - - ### Mouse Lens - lens = root.lens = frame.selectAll "g.lens" - .data [[]] - gLens = lens.enter().append "g" - .attr "class", "lens" - .style "z-index", 1e9 - gInner = gLens.append "g" - .attr "transform", "translate(1.5em,0)" - gInner.append "circle" - .attr "r", "1.5em" - # .style "opacity", "0.4" - # .style "fill", "white" - .style "fill", "rgba(255, 255, 255, 0.4)" - .style "stroke", "white" - .style "stroke-width", "3px" - gInner.append "text" - .attr "y", "0.5em" - .attr "text-anchor", "middle" - .style "fill", "white" - .style "font", "12px Helvetica" - .style "font-weight", "bold" - - - mf = frame.selectAll "g.mf" - .data ["mf"] - .enter().append "g" - .attr "class", "mf" - .append "text" - .attr "class", "yoyo" - .attr "dx", 50 - .attr "dy", 100 - - - - bars.selectAll ".bar" - .on "mouseover", (d, i) -> - el = root.el = el # DOM element of event - # {r,g,b} = color = d3.rgb options.colors[i] - mf - .transition() - .duration(300) - .ease("exp") - .text "Uh boy, the target would be:"+d[1] - .style "font-size", "25px" - - .on "mouseout", (d, i) -> - mf - .transition() - .duration(1000) - .text "BUMMER!!!" - .style "font-size", "0px" - - - # {x:lineX, y:lineY} = root.pt = line.indexToPoint idx - # lens = frame.select "g.lens" - # .attr "transform", "translate(#lineX, #lineY)" - # lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" - # lens.select "text" .text Y - - - - svg diff --git a/lib/chart/type/d3/d3-bar-element.co b/lib/chart/type/d3/d3-bar-element.co deleted file mode 100644 index 9f93fa7..0000000 --- a/lib/chart/type/d3/d3-bar-element.co +++ /dev/null @@ -1,78 +0,0 @@ -d3 = require 'd3' - -{ _, op, -} = require 'kraken/util' -{ D3ChartElement -} = require 'kraken/chart/type/d3/d3-chart-element' - -_fmt = require 'kraken/util/formatters' - -root = do -> this - -class exports.BarChartType extends D3ChartElement - __bind__ : <[ ]> - SPEC_URL : '/schema/d3/d3-bar.json' - - # NOTE: D3ChartElement.register() must come AFTER `typeName` declaration. - chartElement : 'd3-bar' - D3ChartElement.register this - - -> super ... - - renderChartElement: (metric, svgEl ,xScale, yScale) -> - - X = (d, i) -> xScale d[0] - Y = (d, i) -> yScale d[1] - - - ### Render the line path - metricBars = root.metricBars = svgEl.append "g" - .attr "class", "metric bars "+metric.get 'label' - - data = d3.zip metric.getDateColumn(),metric.getData() - - ### Render Bars - barWidth = svgEl.attr('width')/data.length - barHeight = (d) -> svgEl.attr('height')-Y(d) - - metricBars.selectAll "bar" - .data data - .enter().append "rect" - .attr "class", (d, i) -> "metric bar #i" - .attr "x", X - .attr "y", Y - .attr "height", barHeight - .attr "width", barWidth - .attr "fill", metric.get 'color' - .attr "stroke", "white" - .style "opacity", "0.4" - .style "z-index", -10 - - - # adding event listeners - chT = this - metricBars.selectAll ".metric.bar" - .on "mouseover", (d, i) -> - - svgEl.append "text" - .attr "class", "mf" - .attr "dx", 50 - .attr "dy", 100 - .style "font-size", "0px" - .transition() - .duration(800) - .text "Uh boy, the target would be: "+_fmt.numberFormatter(d[1]).toString() - .style "font-size", "25px" - .on "mouseout", (d, i) -> - - svgEl.selectAll ".mf" - .transition() - .duration(300) - .text "BUMMER!!!" - .style "font-size", "0px" - .remove() - - - - svgEl - diff --git a/lib/chart/type/d3/d3-chart-element.co b/lib/chart/type/d3/d3-chart-element.co deleted file mode 100644 index 29cc2a3..0000000 --- a/lib/chart/type/d3/d3-chart-element.co +++ /dev/null @@ -1,84 +0,0 @@ -d3 = require 'd3' -ColorBrewer = require 'colorbrewer' - -{ _, op, -} = require 'kraken/util' -{ ReadyEmitter, -} = require 'kraken/util/event' -# Base = require 'kraken/base/base' - - -root = do -> this - -/** - * Map of known libraries by name. - * @type Object - */ -KNOWN_CHART_ELEMENTS = exports.KNOWN_CHART_ELEMENTS = {} - -class exports.D3ChartElement extends ReadyEmitter - __bind__ : <[ ]> - SPEC_URL : '/schema/d3/d3-chart.json' - - - ### Class Methods - - /** - * Register a new d3 element - */ - @register = (Subclass) -> - # console.log "D3ChartElement.register(#Subclass)" - KNOWN_CHART_ELEMENTS[ Subclass::chartElement ] = Subclass - - /** - * Look up a `charttype` by `typeName`. - */ - @lookup = (name) -> - name = name.get('chartElement') if name instanceof Backbone.Model - KNOWN_CHART_ELEMENTS[name] - - /** - * Look up a chart type by name, returning a new instance - * with the given model (and, optionally, view). - * @returns {D3ChartElement} - */ - @create = (name) -> - # console.log "D3ChartElement.create(#name)" - return null unless Type = @lookup name - - new Type - - - -> - _.bindAll this, ...@__bind__ # TODO: roll up MRO - @loadSpec() unless @ready - super ... - - - - /** - * Load the corresponding chart specification, which includes - * info about valid options, along with their types and defaults. - */ - loadSpec: -> - return this if @ready - proto = @constructor:: - jQuery.ajax do - url : @SPEC_URL - dataType : 'json' - success : (spec) ~> - proto.spec = spec - proto.options_ordered = spec - proto.options = _.synthesize spec, -> [it.name, it] - proto.ready = true - @triggerReady() - error: ~> console.error "Error loading #{@typeName} spec! #it" - - this - - renderChartElement: (metric, svgEl ,xScale, yScale) -> svgEl - - - - - diff --git a/lib/chart/type/d3/d3-geo-element.co b/lib/chart/type/d3/d3-geo-element.co deleted file mode 100644 index 2238b31..0000000 --- a/lib/chart/type/d3/d3-geo-element.co +++ /dev/null @@ -1,185 +0,0 @@ -ColorBrewer = require 'colorbrewer' - -{ _, op, -} = require 'kraken/util' -{ ChartType, -} = require 'kraken/chart/chart-type' - - - - -class exports.GeoWorldChartType extends ChartType - __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> - SPEC_URL : '/schema/d3/d3-geo-world.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'd3-geo-world' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - - - -> super ... - - - transform: -> - options = @model.getOptions() import @determineSize() - # options.colors.palette = ["black", "red"] if options.colors.palette? - options.colors.scaleDomain = d3.extent if options.colors.scaleDomain? - options - - - getProjection : (type) -> - switch type - case 'mercator' 'albers' 'albersUsa' - d3.geo[type]() - case 'azimuthalOrtho' - d3.geo.azimuthal() - .mode 'orthographic' - case 'azimuthalStereo' - d3.geo.azimuthal() - .mode 'stereographic' - default - throw new Error "Invalid map projection type '#type'!" - - - renderChart: (data, viewport, options, lastChart) -> - {width, height} = options - - fill = @fill = (data, options) -> - d3.scale[ options.colors.scale ]() - .domain options.colors.scaleDomain - .range options.colors.palette - - quantize = @quantize = (data, options) -> - (d) -> - if data[d.properties.name]? - return fill data[d.properties.name].editors - else - # console.log 'Country '+d.properties.name+' not in data' - return fill "rgb(0,0,0)" - - projection = @projection = @getProjection(options.map.projection) - .scale width - .translate [width/2, height/2] - - path = d3.geo.path() - .projection projection - - # path objects - feature = map.selectAll ".feature" - infobox = d3.select '.infobox' - - - move = -> - projection - .translate d3.event.translate - .scale d3.event.scale - feature.attr "d", path - - zoom = d3.behavior.zoom() - .translate projection.translate() - .scale projection.scale() - .scaleExtent [height,height*8] - .on "zoom", move - - - #### - - chart = d3.select viewport.0 - .append "svg:svg" - .attr "width", width - .attr "height", height - .append "svg:g" - .attr "transform", "translate(0,0)" - .call zoom - - # rectangle - map.append "svg:rect" - .attr "class", "frame" - .attr "width", width - .attr "height", height - - - ### infobox - infobox.select '#ball' - .append "svg:svg" - .attr "width", "100%" - .attr "height", "20px" - .append "svg:rect" - .attr "width", "60%" - .attr "height", "20px" - .attr "fill", '#f40500' - - setInfoBox = (d) -> - name = d.properties.name - ae = 0 - e5 = 0 - e100 = 0 - - if data[name]? - ae = parseInt data[name].editors - e5 = parseInt data[name].editors5 - e100 = parseInt data[name].editors100 - - infobox.select '#country' .text name - infobox.select '#ae' .text ae - infobox.select '#e5' .text e5+" ("+(100.0*e5/ae).toPrecision(3)+"%)" - infobox.select '#e100' .text e100+" ("+(100.0*e100/ae).toPrecision(3)+"%)" - - xy = d3.svg.mouse this - infobox.style "left", xy[0]+'px' - infobox.style "top", xy[1]+'px' - infobox.style "display", "block" - - - worldmap = -> - d3.json do - "/data/geo/maps/world-countries.json" - (json) -> - feature := feature - .data json.features - .enter().append "svg:path" - .attr "class", "feature" - .attr "d", path - .attr "fill", quantize - .attr "id", (d) -> d.properties.name - .on "mouseover", setInfoBox - .on "mouseout", -> infobox.style "display", "none" - - - - - -data = null -main = -> - jQuery.ajax do - url : "/data/geo/data/en_geo_editors.json" - dataType : 'json' - success : (res) -> - # result will be the returned JSON - data := res - - # delete & hide spinner - jQuery '.geo-spinner' .spin(false).hide() - - # load the world map - worldmap() - - # adding bootstrap tooltips - # $ '.page-header' .tooltip title:"for the header it works but is useless" - # $ '.feature' .tooltip title:"here it doesn't work" - - console.log 'Loaded geo coding map!' - error : (err) -> console.error err - - diff --git a/lib/chart/type/d3/d3-line-element.co b/lib/chart/type/d3/d3-line-element.co deleted file mode 100644 index 51ce742..0000000 --- a/lib/chart/type/d3/d3-line-element.co +++ /dev/null @@ -1,111 +0,0 @@ -d3 = require 'd3' -ColorBrewer = require 'colorbrewer' - -{ _, op, -} = require 'kraken/util' -{ D3ChartElement -} = require 'kraken/chart/type/d3/d3-chart-element' - -_fmt = require 'kraken/util/formatters' - -root = do -> this - -class exports.LineChartElement extends D3ChartElement - __bind__ : <[ ]> - SPEC_URL : '/schema/d3/d3-line.json' - - # NOTE: D3ChartElement.register() must come AFTER `typeName` declaration. - chartElement : 'd3-line' - D3ChartElement.register this - - -> super ... - - renderChartElement: (metric, svgEl ,xScale, yScale) -> - - X = (d, i) -> xScale d[0] - Y = (d, i) -> yScale d[1] - line = d3.svg.line().x(X).y(Y) - - ### Render the line path - metricLine = root.metricLine = svgEl.append "g" - .attr "class", "g metric line "+metric.get 'label' - - data = d3.zip metric.getDateColumn(),metric.getData() - - metricLine.selectAll "path.line" - .data d3.zip data.slice(0,-1), data.slice(1) - .enter().append "path" - .attr "d", line - .attr "class", (d, i) -> "metric line segment #i" - .style "stroke", metric.getColor 'color' - - - ### Mouse Lens - lens = root.lens = svgEl.selectAll "g.lens" - .data [[]] - gLens = lens.enter().append "g" - .attr "class", "lens" - .style "z-index", 1e9 - gInner = gLens.append "g" - .attr "transform", "translate(1.5em,0)" - gInner.append "circle" - .attr "r", "1.5em" - # .style "opacity", "0.4" - # .style "fill", "white" - .style "fill", "rgba(255, 255, 255, 0.4)" - .style "stroke", "white" - .style "stroke-width", "3px" - gInner.append "text" - .attr "y", "0.5em" - .attr "text-anchor", "middle" - .style "fill", "black" - .style "font", "12px Helvetica" - .style "font-weight", "bold" - - # event listeners - metricLine.selectAll ".line.segment" - .on "mouseover", (d, i) -> - - {r,g,b} = color = d3.rgb metric.getColor 'color' - lineX = (X(d[0])+X(d[1]))/2 - lineY = (Y(d[0])+Y(d[1]))/2 - - - lens = svgEl.select "g.lens" - .attr "transform", "translate(#lineX, #lineY)" - lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" - lens.select "text" .text -> _fmt.numberFormatter(d[0][1]).toString() - - svgEl - - -# If instead of a line segment for each month we want to use one line segment per metric, we have to have a way of accessing the data associated with a given time step. - -# lines.attr "d", line -# .attr "class", (col, i) -> "metric line metric#i" -# .style "stroke", (col, i) -> options.colors[i] -# .each (col, i) -> -# {width} = bbox = @getBBox() -# # Add line-to-data position conversions -# @indexAtX = d3.scale.quantize() -# .domain [0, width] -# .range d3.range col.length -# @indexToPoint = (idx) -> -# @pathSegList.getItem idx - -# lines.on "mouseover", (col, i) -> -# line = root.line = this # DOM element of event -# {r,g,b} = color = d3.rgb options.colors[i] - -# # quantize mouse x-location to get for closest data-point (index into data array) -# [x,y] = root.pos = d3.mouse line -# idx = root.idx = line.indexAtX x -# {x:lineX, y:lineY} = root.pt = line.indexToPoint idx - -# lens = frame.select "g.lens" -# .data d3.select(line).data() -# .attr "transform", "translate(#lineX, #lineY)" -# lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" -# lens.select "text" .text (col) -> col[idx][1] - - diff --git a/lib/chart/type/d3/index.co b/lib/chart/type/d3/index.co deleted file mode 100644 index a18badd..0000000 --- a/lib/chart/type/d3/index.co +++ /dev/null @@ -1,6 +0,0 @@ -d3chart = require 'kraken/chart/type/d3/d3-chart-element' -line = require 'kraken/chart/type/d3/d3-line-element' -bar = require 'kraken/chart/type/d3/d3-bar-element' -# geo = require 'kraken/chart/type/d3/d3-geo-element' - -exports import line import bar import d3chart # import geo diff --git a/lib/chart/type/dygraphs.co b/lib/chart/type/dygraphs.co deleted file mode 100644 index 4d0113e..0000000 --- a/lib/chart/type/dygraphs.co +++ /dev/null @@ -1,133 +0,0 @@ -_ = require 'kraken/util/underscore' -{ ChartType, -} = require 'kraken/chart/chart-type' - - -class exports.DygraphsChartType extends ChartType - __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> - SPEC_URL : '/schema/dygraph.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'dygraphs' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - -> super ... - - - - ### Formatters {{{ - - # XXX: Dygraphs-specific - makeAxisFormatter: (fmttr) -> - (n, granularity, opts, g) -> fmttr n, opts, g - - # XXX: Dygraphs-specific - dygAxisDateFormatter: (n, granularity, opts, g) -> - moment(n).format 'MM/YYYY' - - # XXX: Dygraphs-specific - dygDateFormatter: (n, opts, g) -> - moment(n).format 'DD MMM YYYY' - - # XXX: Dygraphs-specific - dygNumberFormatter: (n, opts, g) -> - digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2 - { whole, fraction, suffix } = @numberFormatter n, digits - "#whole#fraction#suffix" - - # XXX: Dygraphs-specific - dygNumberFormatterHTML: (n, opts, g) -> - digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2 - # digits = opts('digitsAfterDecimal') ? 2 - { whole, fraction, suffix } = @numberFormatter n, digits - # coco will trim the whitespace - " - #whole - #fraction - #suffix - " - - - ### }}} - ### Rendering {{{ - - /** - * Determines chart viewport size. - * @return { width, height } - */ - determineSize: -> - modelW = width = @model.get 'width' - modelH = height = @model.get 'height' - return { width, height } unless @view.ready and width and height - - viewport = @getElementsForRole 'viewport' - legend = @getElementsForRole 'legend' - - if width is 'auto' - # Remove old style, as it confuses dygraph after options update - delete viewport.prop('style').width - vpWidth = viewport.innerWidth() or 300 - legendW = legend.outerWidth() or 228 - width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW) - width ?= modelW - - if height is 'auto' - # Remove old style, as it confuses dygraph after options update - delete viewport.prop('style').height - height = viewport.innerHeight() or 320 - height ?= modelH - - { width, height } - - - /** - * Transforms the domain objects into a hash of derived values using - * chart-type-specific keys. - * @returns {Object} The derived chart options. - */ - transform: -> - dataset = @model.dataset - options = @view.chartOptions() import @determineSize() - options import do - colors : dataset.getColors() - labels : dataset.getLabels() - labelsDiv : @getElementsForRole 'legend' .0 - valueFormatter : @dygNumberFormatterHTML - axes: - x: - axisLabelFormatter : @dygAxisDateFormatter - valueFormatter : @dygDateFormatter - y: - axisLabelFormatter : @makeAxisFormatter @dygNumberFormatter - valueFormatter : @dygNumberFormatterHTML - - - /** - * @returns {Dygraph} The Dygraph chart object. - */ - renderChart: (data, viewport, options, lastChart) -> - @resizeViewport() - - # console.log "#this.render!" - # _.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. - lastChart?.destroy() - new Dygraph viewport.0, data, options - - - - ### }}} - - diff --git a/lib/chart/type/index.co b/lib/chart/type/index.co deleted file mode 100644 index e69de29..0000000 diff --git a/lib/dashboard/dashboard-model.co b/lib/dashboard/dashboard-model.co deleted file mode 100644 index d4e3fbf..0000000 --- a/lib/dashboard/dashboard-model.co +++ /dev/null @@ -1,87 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseModel, -} = require 'kraken/base' -{ Graph, GraphList, -} = require 'kraken/graph/graph-model' - - -/** - * @class - */ -Dashboard = exports.Dashboard = BaseModel.extend do # {{{ - urlRoot : '/dashboards' - - # graph_ids : null - graphs : null - # tabs : null - - - constructor: function Dashboard - @graphs = new GraphList - BaseModel ... - - - initialize: -> - BaseModel::initialize ... - # @getGraphs() - - defaults: -> - name : null - tabs : [ { name:"Main", graph_ids:[] } ] - - load: -> - @once 'fetch-success', (~> @getGraphs()) .loadModel() - this - - - /** - * Look up a tab. - * - * @param {String|Number} tab Tab name or index. - * @returns {Tab} Tab object. - */ - getTab : (tab) -> - tabs = @get 'tabs' - return tabs[tab] if typeof tab is 'number' - _.find tabs, -> it.name is tab - -# addGraph: (graph, tabName) -> -# ... - - show : (cb, obj) -> - console.log('[show]') - console.log(obj) - cb null, obj - - pushAsync : (cb, arr) -> - (err, elem) -> - arr.push elem - cb null - - getGraphs : -> - console.log('[getGraphs]\tentering') - # consolidate graph_ids to one array - graph_ids = _(@tabs).chain().values().map((tab_obj) -> tab_obj.graph_ids).flatten().value() - - Seq graph_ids - .parMap_ (next, graph_id) -> - next null, [graph_id] - .parEach_ (next, graph_id_arr) ~> - Graph.lookup graph_id_arr[0], @pushAsync next, graph_id_arr - # Graph.lookup graph_id_arr[0], (err, el) -> - # graph_id_arr.push el - # next null - # .parEach_ @show - .parMap_ (next, [id, graph]:tuple) ~> - graph.once 'ready', -> next.ok tuple - .unflatten() - .seq_ (next, graph_tuples) ~> - # @graphs = _.generate graph_tuples - @graphs.reset _.pluck graph_tuples, 1 - console.log('[setter]\tcalling ready') - @triggerReady() - this - - -# }}} \ No newline at end of file diff --git a/lib/dashboard/dashboard-view.co b/lib/dashboard/dashboard-view.co deleted file mode 100644 index eada37e..0000000 --- a/lib/dashboard/dashboard-view.co +++ /dev/null @@ -1,174 +0,0 @@ -Seq = require 'seq' - -{ _, op, -} = require 'kraken/util' -{ BaseModel, BaseView, -} = require 'kraken/base' -{ Graph, GraphList, GraphDisplayView, -} = require 'kraken/graph' -{ Dashboard, -} = require 'kraken/dashboard/dashboard-model' - - -/** - * @class - */ -DashboardView = exports.DashboardView = BaseView.extend do # {{{ - __bind__ : <[ addTab ]> - tagName : 'section' - className : 'dashboard' - template : require 'kraken/template/dashboard/dashboard' - - - events: - # Select the whole permalink URI text when it receives focus. - 'click .graphs.tabbable .nav a' : 'onTabClick' - 'shown .graphs.tabbable .nav a' : 'render' - # 'shown .graphs.tabbable .nav a' : 'onTabShown' - # 'click a[data-target="#other-graphs"]' : 'onTabShown' - # 'click .load-button' : 'load' - - # subviews : [] - graphs : null - ready : false - - - constructor: function DashboardView(options={}) - @graphs = new GraphList - BaseView ... - - initialize: -> - @model or= new Dashboard - DashboardView.__super__.initialize ... - # @graphs.on 'add', @attachGraphs, this - # @graphs.on 'add', @attachGraph, this - @model.once 'ready', @load, this .load() - - - # FIXME: - # - combine all loads into one seq so... - # - trigger ready when finished - # TODO: - # - only render graph when scrolling makes it visible - load: -> - console.log "#this.load! Model ready!", @model - Seq @model.get('tabs') - .seqEach_ @addTab - .seq ~> - console.log "#{this}.load! Done adding tabs!" - @triggerReady() - - addTab: (nextTab, tab) -> - # self = this - # a(href="#core-graphs", data-toggle="tab") Core - tabModel = new BaseModel tab - tabView = @addSubview new DashboardTabView {model:tabModel} - tabId = tabView.getTabId() - @$ "nav > ul.nav" - .append "
  • #{tab.name}
  • " - - graphs = _(tab.graph_ids).map (graph_id) ~> @model.graphs.get graph_id - Seq graphs - .parMap_ (next, graph) ~> - @graphs.add graph - next null, new GraphDisplayView {model:graph} - .parMap_ (next, graphView) ~> - return next.ok() if graphView.isAttached - tabView.addSubview graphView - # tabEl = @$ tab.name - # if tabEl.length - # tabEl.append view.$el - # view.isAttached = true - # else - # console.log "#this.addTab: Unable to find bind-point for #view!", view - next.ok() - .seq ~> - console.log "#{this}.addTab: All graphs added!" - # @render() - nextTab.ok() - this - - ### Tabs {{{ - - onTabShown: (e) -> - @render() - # @renderSubviews() - # Seq @subviews - # .parMap (view) -> - # # view.resizeViewport() - # view.renderChart() - - onTabClick: (evt) -> - evt.preventDefault() - - - ### }}} - ### Navigation Between Graphs {{{ - - /** - * Scroll to the specified graph. - * - * @param {String|Number|Graph} graph The Graph to scroll to; can be specified as a - * Graph id, an index into the Graphs list, or a Graph object. - * @returns {this} - */ - scrollToGraph: (graph) -> - if typeof graph is 'string' - graph = @graphs.get graph - else if typeof graph is 'number' - graph = @graphs.at graph - unless graph instanceof Graph - console.error "#this.scrollToGraph() Unknown graph #graph!" - return this - - return this unless view = _.find @subviews, -> it.model is graph - $ 'body' .scrollTop view.$el.offset().top if view.$el.is ':visible' - - this - - findClosestGraph: (scroll) -> - scroll or= $ 'body' .scrollTop() - views = @subviews - .filter -> it.$el.is ':visible' - .map -> [ it.$el.offset().top, it ] - .filter -> it[0] >= scroll - .sort (a,b) -> op.cmp a[0], b[0] - return views[0][1] if views.length - - ### }}} - - - -/** - * @class - * @extends BaseView - */ -DashboardTabView = exports.DashboardTabView = BaseView.extend do # {{{ - __bind__ : <[ ]> - className : 'tab-pane' - tag : 'div' - template : require 'kraken/template/dashboard/dashboard-tab' - - - constructor: function DashboardTabView - BaseView ... - - initialize: -> - BaseView::initialize ... - - getTabId: -> - _.underscored @model.get('name') .toLowerCase() + '-graphs-tab' - - toTemplateLocals: -> - json = DashboardTabView.__super__.toTemplateLocals ... - tab_name = _.underscored @model.get('name') .toLowerCase() - json import - tab_cls : "#tab_name-graphs-pane" - tab_id : "#tab_name-graphs-tab" - -# }}} - - - - - diff --git a/lib/dashboard/index.co b/lib/dashboard/index.co deleted file mode 100644 index 03fc595..0000000 --- a/lib/dashboard/index.co +++ /dev/null @@ -1,3 +0,0 @@ -models = require 'kraken/dashboard/dashboard-model' -views = require 'kraken/dashboard/dashboard-view' -exports import models import views diff --git a/lib/data/data-view.co b/lib/data/data-view.co deleted file mode 100644 index c57eaa8..0000000 --- a/lib/data/data-view.co +++ /dev/null @@ -1,122 +0,0 @@ -Seq = require 'seq' -{ _, op, -} = require 'kraken/util' -{ BaseView, ViewList, -} = require 'kraken/base' -{ DataSetView, -} = require 'kraken/data/dataset-view' -{ MetricEditView, -} = require 'kraken/data/metric-edit-view' -{ DataSource, -} = require 'kraken/data/datasource-model' - -/** - * @class DataSet selection and customization UI (root of the `data` tab). - */ -DataView = exports.DataView = BaseView.extend do # {{{ - __bind__ : <[ onMetricsChanged ]> - tagName : 'section' - className : 'data-ui' - template : require 'kraken/template/data/data' - - datasources : null - - - /** - * @constructor - */ - constructor: function DataView - BaseView ... - - initialize: -> - @graph_id = @options.graph_id - BaseView::initialize ... - @metric_views = new ViewList - @datasources = DataSource.getAllSources() - # @on 'update', @onUpdate, this - @model.metrics - .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 - @dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources} - @addSubview @dataset_view - .on 'add-metric', @onMetricsChanged, this - .on 'remove-metric', @onMetricsChanged, this - .on 'select-metric', @selectMetric, this - - @render() - @triggerReady() - this - - - /** - * Transform the `columns` field to ensure an Array of {label, type} objects. - */ - canonicalizeDataSource: (ds) -> - ds.shortName or= ds.name - ds.title or= ds.name - ds.subtitle or= '' - - cols = ds.columns - if _.isArray cols - ds.metrics = _.map cols, (col, idx) -> - if _.isArray col - [label, type] = col - {idx, label, type or 'int'} - else - col - else - ds.metrics = _.map cols.labels, (label, idx) -> - {idx, label, type:cols.types[idx] or 'int'} - ds - - - toTemplateLocals: -> - attrs = _.clone @model.attributes - { @graph_id, @datasources } import attrs - - 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 - .on 'metric-change', @onUpdateMetric, this - @metric_views.push @addSubview view - @renderSubviews() - metric - - removeMetric: (metric) -> - # console.log "#this.removeMetric!", metric - return unless view = @metric_views.findByModel metric - @metric_views.remove view - @removeSubview view - metric - - selectMetric: (metric) -> - # console.log "#this.selectMetric!", metric - @metric_views.invoke 'hide' - @metric_edit_view = @metric_views.findByModel metric - @metric_edit_view?.show() - _.delay @onMetricsChanged, 10 - - onMetricsChanged: -> - return unless @dataset_view - oldMinHeight = parseInt @$el.css 'min-height' - newMinHeight = Math.max do - @dataset_view.$el.height() - @metric_edit_view?.$el.height() - # console.log 'onMetricsChanged!', oldMinHeight, '-->', newMinHeight - @$el.css 'min-height', newMinHeight - - onUpdateMetric: -> - # console.log "#this.onUpdateMetric!" - @trigger 'metric-change', @model, this - @render() - - -# }}} diff --git a/lib/data/dataset-model.co b/lib/data/dataset-model.co deleted file mode 100644 index 5bc6c25..0000000 --- a/lib/data/dataset-model.co +++ /dev/null @@ -1,183 +0,0 @@ -Seq = require 'seq' -ColorBrewer = require 'colorbrewer' - -{ _, op, -} = require 'kraken/util' -{ BaseModel, BaseList, -} = require 'kraken/base' -{ Metric, MetricList, -} = require 'kraken/data/metric-model' -{ DataSource, DataSourceList, -} = require 'kraken/data/datasource-model' - - - -/** - * @class - */ -DataSet = exports.DataSet = BaseModel.extend do # {{{ - urlRoot : '/datasets' - - /** - * @type DataSourceList - */ - sources : null - - /** - * @type MetricList - */ - metrics : null - - defaults : -> - palette : null - lines : [] - metrics : [] - - - constructor: function DataSet (attributes={}, opts) - @metrics = new MetricList attributes.metrics - BaseModel.call this, attributes, opts - - initialize : -> - BaseModel::initialize ... - @set 'metrics', @metrics, {+silent} - @on 'change:metrics', @onMetricChange, this - # @metrics.on 'add remove reset', ~> - # @trigger 'change:metrics', @metrics, this - - - load: (opts={}) -> - @resetReady() if opts.force - return this if @loading or @ready - - unless @metrics.length - return @triggerReady() - - # console.log "#this.load()..." - @wait() - @loading = true - @trigger 'load', this - Seq @metrics.models - .parEach_ (next, metric) -> - metric.once 'ready', next.ok .load() - .seq ~> - # console.log "#{this}.load() complete!" - @loading = false - @unwait() # terminates the `load` wait - @triggerReady() - this - - # refreshSubModels: -> - # # @set 'metrics', @metrics.toJSON(), {+silent} - # @set 'metrics', _.pluck(@metrics.models, 'attributes'), {+silent} - # this - - /** - * Override to handle the case where one of our rich sub-objects - * (basically `metrics`) is set as a result of the `fetch()` call by the - * Graph object. To prevent it from blowing away the `MetricList`, we - * perform a `reset()` here. But that won't trigger a `change:metrics` event, - * so we do a little dance to set it twice, as object identity would otherwise - * cause it to think nothing has changed. - */ - set: (key, value, opts) -> - # return DataSet.__super__.set ... unless @metrics - - if _.isObject(key) and key? - [values, opts] = [key, value] - else - values = { "#key": value } - opts or= {} - - for key, value in values - continue unless key is 'metrics' and _.isArray value - @metrics.reset value - delete values[key] - unless opts.silent - DataSet.__super__.set.call this, 'metrics', value, {+silent} - DataSet.__super__.set.call this, 'metrics', @metrics, opts - - DataSet.__super__.set.call this, values, opts - - - toJSON: -> - json = DataSet.__super__.toJSON ... - delete json.id - json - - - /* * * * TimeSeriesData interface * * * {{{ */ - - /** - * @returns {Array} The reified dataset, materialized to a list of rows including timestamps. - */ - getData: -> - return [] unless @ready - columns = @getColumns() - if columns?.length - _.zip ...columns - else - [] - - /** - * @returns {Array} List of all columns (including date column). - */ - getColumns: -> - return [] unless @ready - _.compact [ @getDateColumn() ].concat @getDataColumns() - - /** - * @returns {Array} The date column. - */ - getDateColumn: -> - return [] unless @ready - dates = @metrics.onlyOk().invoke 'getDateColumn' - maxLen = _.max _.pluck dates, 'length' - _.find dates, -> it.length is maxLen - - /** - * @returns {Array} List of all columns except the date column. - */ - getDataColumns: -> - return [] unless @ready - @metrics.onlyOk().invoke 'getData' - - /** - * @returns {Array} List of column labels. - */ - getLabels: -> - return [] unless @ready - [ 'Date' ].concat @metrics.onlyOk().invoke 'getLabel' - - getColors: -> - return [] unless @ready - @metrics.onlyOk().invoke 'getColor' - - # }}} - - - newMetric: -> - index = @metrics.length - @metrics.add m = new Metric { index, color:ColorBrewer.Spectral[11][index] } - 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}" - @resetReady() - @load() - - - # XXX: toJSON() must ensure columns in MetricList are ordered by index - # ...in theory, MetricList.comparator now does this - - # toJSON: -> - # @refreshSubModels() - # json = DataSet.__super__.toJSON ... - # json.metrics = json.metrics.map -> it.toJSON?() or it - # json - -# }}} - diff --git a/lib/data/dataset-view.co b/lib/data/dataset-view.co deleted file mode 100644 index 4a55a14..0000000 --- a/lib/data/dataset-view.co +++ /dev/null @@ -1,155 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseView, -} = require 'kraken/base' - - -/** - * @class - */ -DataSetView = exports.DataSetView = BaseView.extend do # {{{ - tagName : 'section' - className : 'dataset-ui dataset' - template : require 'kraken/template/data/dataset' - - events: - 'click .new-metric-button' : 'onNewMetric' - 'click .delete-metric-button' : 'onDeleteMetric' - 'click .metrics .dataset-metric' : 'selectMetric' - - views_by_cid : {} - active_view : null - - - constructor: function DataSetView - BaseView ... - - initialize: -> - {@graph_id, @datasources, @dataset} = @options - BaseView::initialize ... - @views_by_cid = {} - @model - .on 'ready', @addAllMetrics, this - @model.metrics - .on 'add', @addMetric, this - .on 'remove', @removeMetric, this - .on 'change', @onMetricChange, this - .on 'reset', @addAllMetrics, 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 - @trigger 'add-metric', metric, view, this - @render() - view - - 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] - @trigger 'remove-metric', metric, view, this - view - - addAllMetrics: -> - # console.log "#this.addAllMetrics! --> #{@model.metrics}" - @removeAllSubviews() - @model.metrics.each @addMetric, this - this - - - selectMetric: (metric) -> - if metric instanceof [jQuery.Event, Event] - 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.$ '.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' - -# }}} - - - -/** - * @class - */ -DataSetMetricView = exports.DataSetMetricView = BaseView.extend do # {{{ - tagName : 'tr' - className : 'dataset-metric metric' - template : require 'kraken/template/data/dataset-metric' - - - - constructor: function DataSetMetricView - BaseView ... - - initialize: -> - @graph_id = @options.graph_id - BaseView::initialize ... - @on 'update', @onUpdate, this - - - toTemplateLocals: -> - m = DataSetMetricView.__super__.toTemplateLocals ... - - # XXX: Icons/classes for visible/disabled? - m import - graph_id : @graph_id - label : @model.getLabel() - viewClasses : _.compact([ - if @model.isOk() then 'valid' else 'invalid', - if m.visible then 'visible' else 'hidden', - 'disabled' if m.disabled, - ]).map( -> "metric-#it" ).join ' ' - source : - if m.source_id and m.source_col - "#{m.source_id}[#{m.source_col}]" - else - 'No source' - timespan : - if _.every ts = m.timespan, op.ok - "#{ts.start} to #{ts.end} by #{ts.step}" - else - '—' - - onUpdate: -> - @$ '.col-color' .css 'color', @model.get 'color' - -# }}} - diff --git a/lib/data/datasource-model.co b/lib/data/datasource-model.co deleted file mode 100644 index 544af94..0000000 --- a/lib/data/datasource-model.co +++ /dev/null @@ -1,190 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ TimeSeriesData, CSVData, -} = require 'kraken/util/timeseries' -{ BaseModel, BaseList, ModelCache, -} = require 'kraken/base' -{ Metric, MetricList, -} = require 'kraken/data/metric-model' - - -/** - * @class - */ -DataSource = exports.DataSource = BaseModel.extend do # {{{ - __bind__ : <[ onLoadDataSuccess onLoadDataError ]> - urlRoot : '/datasources' - ready : false - - /** - * Parsed data for this datasource. - * @type Array - */ - data : null - - defaults: -> - id : '' - url : '' - format : 'json' - - name : '' - shortName : '' - title : '' - subtitle : '' - desc : '' - notes : '' - - timespan : - start : null - end : null - step : '1mo' - - columns : [] - - chart : - chartType : 'dygraphs' - options : {} - - url: -> - "/datasources/#{@id}.json" - - - - - - constructor: function DataSource - BaseModel ... - - initialize: -> - @attributes = @canonicalize @attributes - BaseModel::initialize ... - @constructor.register this - @metrics = new MetricList @attributes.metrics - @on 'change:metrics', @onMetricChange, this - - - canonicalize: (ds) -> - ds.shortName or= ds.name - ds.title or= ds.name - ds.subtitle or= '' - - cols = ds.columns - if _.isArray cols - ds.metrics = _.map cols, (col, idx) -> - if _.isArray col - [label, type] = col - {idx, label, type or 'int'} - else - col.type or= 'int' - col - else - ds.metrics = _.map cols.labels, (label, idx) -> - {idx, label, type:cols.types[idx] or 'int'} - ds - - - - 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' then @loadJSON() - case 'csv' then @loadCSV() - default - console.error "#this.load() Unknown Data Format!" - @onLoadDataError null, 'Unknown Data Format!', new Error 'Unknown Data Format!' - this - - loadJSON: -> - $.ajax do - url : @get 'url' - dataType : 'json' - success : (data) ~> @onLoadDataSuccess new TimeSeriesData data - error : @onLoadDataError - this - - loadCSV: -> - $.ajax do - url : @get 'url' - dataType : 'text' - success : (data) ~> @onLoadDataSuccess new CSVData data - error : @onLoadDataError - this - - onLoadDataSuccess: (@data) -> - # 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 ''}" - @unwait() - @_errorLoading = true - @trigger 'load-data-error', this, txtStatus, err - - - getDateColumn: -> - @data?.dateColumn - - getData: -> - @data?.toJSON?() or @data - - getColumn: (idx) -> - @data?.columns[idx] - - getColumnName: (idx) -> - @get('metrics')?[idx]?.label - - getColumnIndex: (name) -> - return that.idx if _.find @get('metrics'), -> it.label is name - -1 - - onMetricChange: -> - @metrics.reset @get 'metrics' - - -# }}} - - -/** - * @class - */ -DataSourceList = exports.DataSourceList = BaseList.extend do # {{{ - urlRoot : '/datasources' - model : DataSource - - constructor: function DataSourceList then BaseList ... - initialize : -> BaseList::initialize ... -# }}} - - - -### DataSource Cache - -ALL_SOURCES = new DataSourceList -sourceCache = new ModelCache DataSource, {-ready, cache:ALL_SOURCES} - -# Fetch all DataSources -$.getJSON '/datasources/all', (data) -> - ALL_SOURCES.reset _.map data, op.I - sourceCache.triggerReady() - -DataSource.getAllSources = -> - ALL_SOURCES - - diff --git a/lib/data/datasource-ui-view.co b/lib/data/datasource-ui-view.co deleted file mode 100644 index 7f04041..0000000 --- a/lib/data/datasource-ui-view.co +++ /dev/null @@ -1,69 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseModel, BaseList, BaseView, -} = require 'kraken/base' - - -/** - * @class - * Model is a Metric. - */ -DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{ - __bind__ : <[ ]> - tagName : 'section' - className : 'datasource-ui' - template : require 'kraken/template/data/datasource-ui' - - events : - 'click .datasource-summary' : 'onHeaderClick' - 'click .datasource-source-metric' : 'onSelectMetric' - - graph_id : null - dataset : null - datasources : null - - - - constructor: function DataSourceUIView - BaseView ... - - initialize: -> - this import @options.{graph_id, dataset, datasources} - BaseView::initialize ... - - toTemplateLocals: -> - locals = @model.toJSON() - locals import {@graph_id, @dataset, @datasources, cid:@model.cid} - - ds = @model.source - hasSource = @model.get('source_id')? and ds - locals.source_summary = unless hasSource then '' else @model.getSourceColumnName() - - dsts = ds?.get('timespan') or {} - ts = locals.timespan = _.defaults _.clone(@model.get('timespan')), dsts - hasTimespan = hasMetric and ts.start and ts.end and ts.step - locals.timespan_summary = unless hasTimespan then ' - p.help-block A description of the graph. - - .graph-data-pane.tab-pane(id="#{graph_id}-tab-data", data-subview="DataView") - //- - .row-fluid - label.dataset.control-label(for="#{graph_id}_dataset") Data Set - input.span3.dataset(type='text', id="#{graph_id}_dataset", name='dataset', placeholder='URL to dataset file', value=dataset) - p.help-block This dataset filename will soon be replaced by a friendly UI. - - .graph-options-pane.tab-pane(id="#{graph_id}-tab-options", data-subview="ChartOptionScaffold") - diff --git a/lib/template/graph/graph-list.jade b/lib/template/graph/graph-list.jade deleted file mode 100644 index ca8951d..0000000 --- a/lib/template/graph/graph-list.jade +++ /dev/null @@ -1,7 +0,0 @@ -section#graph-list - .page-header - h1 Saved Graphs - ul - for graph in collection.models - li - a(href="#{graph.toLink()}") #{graph.get('name')} diff --git a/lib/util/aliasdict.co b/lib/util/aliasdict.co deleted file mode 100644 index 4df93b6..0000000 --- a/lib/util/aliasdict.co +++ /dev/null @@ -1,158 +0,0 @@ -_ = require 'kraken/util/underscore' - -/** - * @class A mapping of key-value pairs supporting key-aliases. - */ -class AliasDict - - /** - * Data store. - * @type Object - * @private - */ - _data : null - - /** - * Mapping from keys to an array of [potentially nested] alias-keys. - * @type Object> - * @private - */ - _aliases : null - - - /** - * @constructor - */ - -> - @_data = {} - @_aliases = {} - @extend ... - - - /** - * @returns {Number} Number of real keys in the Dict. - */ - size : -> - _.keys @_data .length - - /** - * @returns {AliasDict} A copy of the AliasDict, including aliases as well as data. - */ - clone: -> - d = new AliasDict @_data - _.each @_aliases, (v, k) -> - d.setAlias k, v.slice() - d - - - - ### Value Accessors ### - - /** - * @returns {Boolean} Whether there is a value at the given key. - */ - has : (key) -> - (@get key)? - - /** - * @returns {*} Ignores aliases, returning the value at key or `undefined`. - */ - getValue : (key) -> - prop = _.getNested @_data, key - prop.value if prop? - - get : (key, def) -> - aliases = @_aliases[key] or [key] - val = aliases.reduce do - (val, alias) -> - return val if val? is not undefined - prop = _.getNested @_data, alias - prop.value if prop? - undefined - - if val is not undefined - val - else - def - - set : (key, val) -> - _.setNested @_data, key, val, {+ensure} - val - - del : (key) -> - prop = _.getNestedMeta key - if prop - delete prop.obj[prop.key] - prop.value - - - - ### Alias Methods ### - - hasAlias : (key) -> - @_aliases[key]? - - getAlias : (key, def=[]) -> - @_aliases[key] or def - - setAlias: (key, aliases) -> - @_aliases[key] = if _.isArray aliases then aliases else [aliases] - this - - addAlias : (key, ...aliases) -> - @_aliases[key] = _.flatten @getAlias(key, [key]).concat(aliases) - this - - delAlias : (key) -> - delete @_aliases[key] - - - - ### Collection Methods ### - - toObject: -> - _.clone @_data - - keys: -> - _.keys @_data - - values: -> - _.values @_data - - extend : (...args) -> - for o of args - for k,v in o then @set k, v - this - - reduce : (fn, acc, context=this) -> - _.reduce @_data, fn, acc, context - - map : (fn, context=this) -> - _.map @_data, fn, context - - filter: (fn, context=this) -> - _.filter @_data, fn, context - - each : (fn, context=this) -> - _.each @_data, fn, context - this - - invoke : (name, ...args) -> - _.invoke @_data, name, ...args - - pluck : (attr) -> - _.pluck @_data, attr - - find: (fn, context=this) -> - _.find @_data, fn, context - - - toString: -> - Cls = @.constructor - "#{Cls.displayName or Cls.name}()" - - -module.exports = exports = AliasDict - - - diff --git a/lib/util/backbone.co b/lib/util/backbone.co deleted file mode 100644 index e3d5d28..0000000 --- a/lib/util/backbone.co +++ /dev/null @@ -1,128 +0,0 @@ -### Patches to make Backbone work with browserify - -# Expose Underscore so Backbone can find it -_ = require 'underscore' -window?._ = _ - -# Expose Backbone so plugins can find it -Backbone = require 'backbone' -window?.Backbone = Backbone - -# Pass jQuery to Backbone, as it only looks on its module if `require` is defined -Backbone.setDomLibrary that if window? and (window.jQuery or window.Zepto or window.ender) - - -_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 - this - - /** - * Compatibility with Node's `EventEmitter`. - */ - emit: Backbone.Events.trigger - - - -/** - * @namespace Meta-utilities for working with Backbone classes. - */ -_backbone = do - - # /** - # * Mix the given object or prototype into this class - # */ - # addMixin: (mixin) -> - # ... - - /** - * @returns {Array} The list of all superclasses for this class or object. - */ - getSuperClasses: function getSuperClasses(Cls) - return [] unless Cls - - if Cls.__superclass__ - superclass = that - else - 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 - else - [] - - /** - * Looks up an attribute on the prototype of each class in the class - * hierarchy. - * @returns {Array} - */ - pluckSuper: (obj, prop) -> - return [] unless obj - _ _backbone.getSuperClasses(obj) .chain() - .pluck 'prototype' - .pluck prop - .value() - - /** - * As `.pluckSuper()` but includes value of `prop` on passed `obj`. - * @returns {Array} - */ - pluckSuperAndSelf: (obj, prop) -> - return [] unless obj - [ obj[prop] ].concat _backbone.pluckSuper(obj, prop) - - -exports import _backbone - - - -/** - * Decorates a function so that its receiver (`this`) is always added as the - * first argument, followed by the call arguments. - * @returns {Function} - */ -methodize = exports.methodize = (fn) -> - m = fn.__methodized__ - return m if m - - g = fn.__genericized__ - return that if g?.__wraps__ - - m = fn.__methodized__ = (...args) -> - args.unshift this - fn.apply this, args - - m.__wraps__ = fn - m - - -# Add the class extensions as static methods of the Backbone classes -_methodized = exports._methodized = _.reduce do - _backbone - (o, v, k) -> - o[k] = if typeof v is 'function' then methodize v else v - o - {} - - -_.extend Backbone.Events, _bb_events -for Cls of Backbone.<[ Model Collection View ]> - Cls import _methodized import _bb_events import Backbone.Events - Cls:: import _methodized import _bb_events - diff --git a/lib/util/bitstring.co b/lib/util/bitstring.co deleted file mode 100644 index a5be754..0000000 --- a/lib/util/bitstring.co +++ /dev/null @@ -1,247 +0,0 @@ -SEEK_ABSOLUTE = 0 -SEEK_RELATIVE = 1 -SEEK_FROM_EOF = 2 - - -# Binary representation of the number -bin = (n) -> - do - s = (if n % 2 then '1' else '0') + (s or '') - n >>= 1 - while n - s - -# Number of bits needed to represent the absolute value of n. -binlen = (n) -> - bin Math.abs n .length - -# Returns a run of 1s of size n. -mask = (n) -> - (1 << n) - 1 - -chr = -> String.fromCharCode it -ord = -> String(it).charCodeAt 0 - - - - -/** - * File-like object for reading/writing bits. - * @class - */ -class BitString - # Array - buf : null - - # Byte position of read/write cursor (-1 for end). - _pos : -1 - - # Spill cache for bits smaller than a byte waiting to write. - _spill : 0 - - # Number of bits in the spill cache. - _spillen : 0 - - # Peek cache for read requests smaller than a byte. - _peek : 0 - - # Number of bits in the peek cache. - _peeklen : 0 - - - - (source='', buf=[]) -> - @buf = buf.slice() - for i til source.length - @_bufwrite source.charCodeAt i - - size: -> - @buf.length + if @_spillen then 1 else 0 - - bitsize: -> - @buf.length*8 + @_spillen - - _bufwrite: (b) -> - if @_pos is -1 - @buf.push b - else - @buf[@_pos] = b - @_pos = -1 if ++@_pos >= @buf.length - this - - # Writes bits to the stream; bits must be supplied as a number. Supplying n=0 will write one bit. - # Supplying the optional parameter length treats the bits as a field with the given length. - writebits: (n, size) -> - size = size or binlen n - bits = (@_spill << size) | n - size += @_spillen # handles _spill=0 but _spillen > 0 - while size >= 8 - size -= 8 - b = bits >> size - bits &= mask size - @_bufwrite b - @_spill = bits - @_spillen = size - this - - # Flushes any pending bits to the stream. - flush: -> - b = @_spill - if @_spillen - b <<= 8 - @_spillen - @_bufwrite b - @_spill = 0 - @_spillen = 0 - this - - # Truncates the stream to zero bits. - truncate: -> - @buf = [] - @_pos = -1 - @_spill = 0 - @_spillen = 0 - @_peek = 0 - @_peeklen = 0 - this - - # Move buffer cursor to given byte-offset. mode: 0 = absolute, 1 = relative, 2 = relative EOF - _bufseek: (n, mode=SEEK_ABSOLUTE) -> - switch mode - case 1 # relative - pos = @_pos + n - case 2 - pos = @buf.length + n - default # absolute - pos = n - @_pos = if pos >= @buf.length then -1 else Math.max 0, pos - this - - # Flushes the bit-buffer and moves to the given byte-offset. mode: 0 = absolute, 1 = relative, 2 = relative EOF - seek: (n, mode=SEEK_ABSOLUTE) -> - @flush() - @_peek = 0 - @_peeklen = 0 - @_bufseek n, mode - this - - # Returns the current position of the cursor as a *byte* offset from the start of the stream. - tell: -> - if @_pos is -1 then @buf.length else @_pos - - - _nextbyte: -> - return null if @_pos is -1 - byte = @buf[ @_pos++ ] - @_pos = -1 if @_pos >= @buf.length - byte - - - # Reads n bits from the stream. - readbits: (n) -> - return 0 if n == 0 - - size = @_peeklen - bits = @_peek - - while size < n - byte = @_nextbyte() - break unless byte? - size += 8 - bits = (bits << 8) | byte - - if size > n - @_peeklen = size - n - @_peek = bits & mask(@_peeklen) - bits >>= @_peeklen - else - @_peeklen = 0 - @_peek = 0 - - return if size then bits else null - - - # Reads the next n bits without moving the cursor. - peek: (n) -> - offset = 0 - size = @_peeklen - bits = @_peek - - while size < n - byte = @_nextbyte() - break unless byte? - offset += 1 - size += 8 - bits = (bits << 8) | byte - - if size == 0 - return null - - if size > n - bits >>= size - n - - if offset - @_bufseek -offset, SEEK_RELATIVE - bits - - - # True if there is more data to read. - hasMore: -> - @peek(1)? - - - ### XXX: Should .each(), .map(), .reduce() flush? - - # forEach of bytes - each: (fn, cxt=this) -> - @buf.forEach fn, cxt - - # map over bytes - map: (fn, cxt=this) -> - @buf.map fn, cxt - - # reduce over bytes - reduce: (fn, acc, cxt=this) -> - fn .= bind this - @buf.reduce fn, acc - - - # Returns the stream as a bytearray. - bytearray: -> - @flush().buf.slice() - - # Dumps the stream as a binary string. Unlike __index__(), bin() will not cause int overflow. - bin: (byte_sep='') -> - @flush().buf.map(bin).join(byte_sep) - - # Returns the stream as a hex string. - hex: -> - @flush().buf.map(hex).join('') - - # Returns the buffer as a number. Use this with obvious caution. Called by builtins bin(), int(), long(), etc. - number: -> - @flush() - @reduce (n, byte) -> (n << 8) | byte - - - # Dumps the stream as a string; does not flush or change cursor position. - dump: -> - @buf.map(chr).join('') + if @_spillen then chr @_spill << (8 - @_spillen) else '' - - repr: (dump_buf=true) -> - s = if dump_buf then "buf=#{@dump()}" else "len(buf)=#{@buf.length}" - return "BitString(#s, - spill[#{@_spillen}]=#{bin @_spill}, - tell=#{@tell()}, - peek[#{@_peeklen}]=#{bin @_peek}) - " - - # Dumps the stream as a string; flushes the bit-buffer but leaves cursor position unchanged. - toString: -> - @flush().dump() - - -exports = module.exports = BitString - -exports.SEEK_ABSOLUTE = SEEK_ABSOLUTE -exports.SEEK_RELATIVE = SEEK_RELATIVE -exports.SEEK_FROM_EOF = SEEK_FROM_EOF diff --git a/lib/util/cascade.co b/lib/util/cascade.co deleted file mode 100644 index 1c09368..0000000 --- a/lib/util/cascade.co +++ /dev/null @@ -1,379 +0,0 @@ -_ = require 'kraken/util/underscore' - -hasOwn = ({}).hasOwnProperty - -/** - * Sentinel for missing values. - */ -MISSING = void - -/** - * Tombstone for deleted, non-passthrough keys. - */ -TOMBSTONE = {} - - -/** - * @class A mapping of key-value pairs supporting lookup fallback across multiple objects. - */ -class Cascade - /** - * Sentinel tombstone for deleted, non-passthrough keys. - * @type TOMBSTONE - * @readonly - */ - @TOMBSTONE = TOMBSTONE - - - /** - * Map holding the object's KV-pairs; always the second element of the - * cascade lookup. - * @type Object - * @private - */ - _data : null - - /** - * Map of tombstones, marking intentionally unset keys in the object's - * KV-pairs; always the first element of the cascade lookup. - * @type Object - * @private - */ - _tombstones : null - - /** - * List of objects for lookups. - * @type Array - * @private - */ - _lookups : null - - - - /** - * @constructor - */ - (data={}, lookups=[], tombstones={}) -> - @_data = data - @_tombstones = tombstones - @_lookups = [@_data].concat lookups - - - /** - * @returns {Cascade} A copy of the data and lookup chain. - */ - clone: -> - new Cascade do - {} import @_data - @_lookups.slice() - {} import @_tombstones - - - - ### Data & Lookups ### - - getData: -> - @_data - - setData: (data) -> - @_data = @_lookups[0] = data - this - - getTombstones: -> - @_tombstones - - # setTombstones: (tombstones) -> - # @_tombstones = tombstones - # for k, v in _.collapseObject @_tombstones - # if v - # _.setNested @_tombstones, k, TOMBSTONE, {+ensure} - # else - # _.unsetNested @_tombstones, k - # this - - /** - * @returns {Number} Number of lookup dictionaries. - */ - size: -> - @_lookups.length - 1 - - /** - * @returns {Array} The array of lookup dictionaries. - */ - getLookups: -> - @_lookups - - /** - * @returns {Array} The array of lookup dictionaries. - */ - getLookups: -> - @_lookups - - /** - * Adds a new lookup dictionary to the chain. - * @returns {this} - */ - addLookup: (dict) -> - return this unless dict? - throw new Error "Lookup dictionary must be an object! dict=#dict" unless _.isObject dict - @_lookups.push dict - this - - /** - * Removes a lookup dictionary from the chain (but will not remove the data object). - * @returns {this} - */ - removeLookup: (dict) -> - _.remove @_lookups, dict if dict and dict is not @_data - this - - /** - * Pops the last dictionary off the lookup chain and returns it. - * @returns {*} The last dictionary, or `undefined` if there are no additional lookups. - */ - popLookup: -> - return if @size() <= 1 - @_lookups.pop() - - /** - * Shifts the first additional lookup dictionary off the chain and returns it. - * @returns {*} The first dictionary, or `undefined` if there are no additional lookups. - */ - shiftLookup: -> - return if @size() <= 1 - @_lookups.splice(1, 1)[0] - - /** - * Adds a lookup dictionary to the front of the chain, just after the Cascade's own data - * object. - * @returns {this} - */ - unshiftLookup: (dict) -> - return this unless dict? - throw new Error "Lookup dictionary must be an object! dict=#dict" unless _.isObject dict - @_lookups.splice 1, 0, dict - this - - - /** - * @returns {Boolean} Whether there is a tombstone set for `key`. - */ - hasTombstone: (key) -> - o = @_tombstones - for part of key.split('.') - o = o[part] - return true if o is TOMBSTONE - return false unless o - false - - /** - * @returns {Boolean} Whether `key` belongs to this object (not inherited - * from the cascade). - */ - isOwnProperty: (key) -> - return true if @hasTombstone key - meta = _.getNestedMeta(@_data, key) - meta?.obj and hasOwn.call meta.obj, key - - /** - * @returns {Boolean} Whether `key` belongs to this object (not inherited - * from the cascade) and is defined. - */ - isOwnValue: (key) -> - not @hasTombstone key - and @isOwnProperty key - and _.getNested(@_data, key, MISSING) is not MISSING - - /** - * @returns {Boolean} Whether the value at `key` is the same as that - * inherited by from the cascade. - */ - isInheritedValue: (key, strict=false) -> - return false if @hasTombstone key - val = @get key - cVal = @_getInCascade key, MISSING, 2 - if strict - val is cVal - else - _.isEqual val, cVal - - /** - * @returns {Boolean} Whether the value at `key` is different from that - * inherited by from the cascade. - */ - isModifiedValue: (key, strict=false) -> - not @isInheritedValue key, strict - - - - ### Value Accessors ### - - /** - * @private - * @param {String} key Key to look up. - * @param {*} [def=undefined] Value to return if lookup fails. - * @param {Number} [idx=0] Index into lookup list to begin search. - * @returns {*} First value for `key` found in the lookup chain starting at `idx`, - * and `def` otherwise. - */ - _getInCascade : (key, def, idx=0) -> - return def if @hasTombstone key - - lookups = if idx then @_lookups.slice(idx) else @_lookups - for data of lookups - val = _.getNested data, key, MISSING, {tombstone:TOMBSTONE} - return def if val is TOMBSTONE - return val unless val is MISSING - def - - /** - * @returns {Boolean} Whether there is a value at the given key. - */ - has : (key) -> - @get(key, MISSING) is not MISSING - - /** - * @param {String} key Key to look up. - * @param {*} [def=undefined] Value to return if lookup fails. - * @returns {*} First value for `key` found in the lookup chain, - * and `def` otherwise. - */ - get : (key, def) -> - @_getInCascade key, def - - /** - * Sets a key to a value, accepting nested keys and creating intermediary objects as necessary. - * @public - * @name set - * @param {String} key Key to set. - * @param {*} value Non-`undefined` value to set. - * @returns {this} - */ - /** - * @public - * @name set - * @param {Object} values Map of pairs to set. No value may be `undefined`. - * @returns {this} - */ - set : (values) -> - # Handle @set(k, val) - if arguments.length > 1 and typeof values is 'string' - [key, val] = arguments - throw new Error("Value and key cannot be undefined!") if not key or val is void - values = { "#key": val } - - # Set and ensure the creation of missing intermediate objects. - for key, val in values - _.unsetNested @_tombstones, key, {+ensure} - _.setNested @_data, key, val, {+ensure} - - this - - - /** - * Delete the given key from this object's data dictionary and set a tombstone - * which ensures that future lookups do not cascade and thus see the key as - * `undefined`. - * - * If the key is missing from the data dictionary the delete does not cascade, - * but the tombstone is still set. - * - * @param {String} key Key to unset. - * @returns {undefined|*} If found, returns the old value, and otherwise `undefined`. - */ - unset: (key) -> - old = @get key - _.unsetNested @_data, key - _.setNested @_tombstones, key, TOMBSTONE, {+ensure} - old - - - /** - * Unsets the key in the data dictionary, but ensures future lookups also - * see the key as `undefined`, as opposed. - * - * @param {String} key Key to unset. - * @returns {this} - */ - inherit: (key) -> - _.unsetNested @_tombstones, key, {+ensure} - _.unsetNested @_data, key - - - - ### Collection Methods ### - - extend : -> - for o of arguments then @set o - this - - /** - * Recursively collapses the Cascade to a plain object by recursively merging the - * lookups (in reverse order) into the data. - * @returns {Object} - */ - collapse: -> - o = _.merge {}, ...@_lookups.slice(1).reverse() - for k in @_tombstones - delete o[k] - _.merge o, @_data - - /** - * Returns a plain object for JSON serialization via {@link Cascade#collapse()}. - * The name of this method is a bit confusing, as it doesn't actually return a - * JSON string -- but I'm afraid that it's the way that the JavaScript API for - * `JSON.stringify()` works. - * - * @see https://developer.mozilla.org/en/JSON#toJSON()_method - * @return {Object} Plain object for JSON serialization. - */ - toJSON: -> - @collapse() - - # XXX: should unique? but then won't map 1:1 to @values()... - keys: -> - _.flatten _.map @_lookups, -> _.keys it - - values: -> - _.flatten _.map @_lookups, -> _.values it - - reduce : (fn, acc, context=this) -> - _.reduce @_lookups, fn, acc, context - - map : (fn, context=this) -> - _.map @_lookups, fn, context - - filter: (fn, context=this) -> - _.filter @_lookups, fn, context - - each : (fn, context=this) -> - _.each @_lookups, fn, context - this - - invoke : (name, ...args) -> - _.invoke @_lookups, name, ...args - - pluck : (attr) -> - _.pluck @_lookups, attr - - find: (fn, context=this) -> - _.find @_lookups, fn, context - - - - toString: -> - Cls = this.constructor - "#{Cls.displayName or Cls.name}()" - - -# Alias methods to alternate names -ALIASES = - setTombstone : 'unset' - toObject : 'collapse' - forEach : 'each' - -for dest, src in ALIASES - Cascade::[dest] = Cascade::[src] - - -module.exports = exports = Cascade diff --git a/lib/util/crc.co b/lib/util/crc.co deleted file mode 100644 index 056b090..0000000 --- a/lib/util/crc.co +++ /dev/null @@ -1,58 +0,0 @@ - -crc32 = exports.crc32 = (s, last_crc=0) -> - s = utf8Encode s - crc = last_crc ^ (-1) - for i til s.length - y = (crc ^ s.charCodeAt i) & 0xFF - x = "0x" + TABLE.substr y*9, 8 - crc = (crc >>> 8) ^ x - crc ^ (-1) - - -utf8Encode = exports.utf8Encode = (s) -> - s = s.replace /\r\n/g, '\n' - u = '' - for n til s.length - c = s.charCodeAt n - if c < 128 - u += String.fromCharCode c - else if 127 < c < 2048 - u += String.fromCharCode (c >> 6) | 192 - u += String.fromCharCode (c & 63) | 128 - else - u += String.fromCharCode (c >> 12) | 224 - u += String.fromCharCode ((c >> 6) & 63) | 128 - u += String.fromCharCode (c & 63) | 128 - u - - -# static precompiled hashes -TABLE = ''' - 00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 - E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE - 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 - FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B - 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A - C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 - 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F - 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 - 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 - 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 - 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 - AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F - 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 - 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 - E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB - 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 - D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C - 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 - CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 - 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 - 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 - 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C - 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 - 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 - BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 - 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D -''' - diff --git a/lib/util/event/index.co b/lib/util/event/index.co deleted file mode 100644 index 18c3e40..0000000 --- a/lib/util/event/index.co +++ /dev/null @@ -1,2 +0,0 @@ -exports.WaitingEmitter = require 'kraken/util/event/waiting-emitter' -exports.ReadyEmitter = require 'kraken/util/event/ready-emitter' diff --git a/lib/util/event/ready-emitter.co b/lib/util/event/ready-emitter.co deleted file mode 100644 index 049ad1c..0000000 --- a/lib/util/event/ready-emitter.co +++ /dev/null @@ -1,54 +0,0 @@ -Base = require 'kraken/base/base' - - -/** - * @class An EventEmitter that auto-triggers new handlers once "ready". - */ -class ReadyEmitter extends Base - readyEventName : 'ready' - ready : false - - /** - * Triggers the 'ready' event if it has not yet been triggered. - * Subsequent listeners added to this event will be auto-triggered. - * @param {Boolean} [force=false] Trigger the event even if already ready. - * @returns {this} - */ - triggerReady: (force) -> - return this if @ready and not force - @ready = true - @emit @readyEventName, this - this - - /** - * Resets the 'ready' event to its non-triggered state, firing a - * 'ready-reset' event. - * @param {Boolean} [force=false] Trigger the event even if already reset. - * @returns {this} - */ - resetReady: (force) -> - return this unless @ready and not force - @ready = false - @emit "#{@readyEventName}-reset", this - this - - - /** - * Wrap {@link EventEmitter#on} registration to handle registrations - * on 'ready' after we've broadcast the event. Handler will always still - * be registered, however, in case the emitter is reset. - * - * @param {String} events Space-separated events for which to register. - * @param {Function} callback - * @param {Object} [context] - * @returns {this} - */ - on: (events, callback, context=this) -> - return this if not callback - super ... - if @ready and -1 is not events.split(/\s+/).indexOf @readyEventName - setTimeout ~> callback.call context, this - this - - -module.exports = exports = ReadyEmitter diff --git a/lib/util/event/waiting-emitter.co b/lib/util/event/waiting-emitter.co deleted file mode 100644 index 3f29ddf..0000000 --- a/lib/util/event/waiting-emitter.co +++ /dev/null @@ -1,55 +0,0 @@ -Base = require 'kraken/base/base' - - -/** - * @class An EventEmitter with a ratchet-up waiting counter. - * @extends Base - */ -class WaitingEmitter extends Base - - /** - * Count of outstanding tasks. - * @type Number - */ - waitingOn : 0 - - - /** - * Increment the waiting task counter. - * @returns {this} - */ - wait: -> - count = @waitingOn - @waitingOn += 1 - # console.log "#this.wait! #count --> #{@waitingOn}" - # console.trace() - @trigger('start-waiting', this) if count is 0 and @waitingOn > 0 - this - - /** - * Decrement the waiting task counter. - * @returns {this} - */ - unwait: -> - count = @waitingOn - @waitingOn -= 1 - # console.warn "#this.unwait! #{@waitingOn} < 0" if @waitingOn < 0 - # console.log "#this.unwait! #count --> #{@waitingOn}" - # console.trace() - @trigger('stop-waiting', this) if @waitingOn is 0 and count > 0 - this - - /** - * @param {Function} fn Function to wrap. - * @returns {Function} A function wrapping the passed function with a call - * to `unwait()`, then delegating with current context and arguments. - */ - unwaitAnd: (fn) -> - self = this - -> - # console.log "#self.unwaitAnd( function #{fn.name or fn.displayName}() )" - # console.trace() - self.unwait(); fn ... - - -module.exports = exports = WaitingEmitter diff --git a/lib/util/formatters.co b/lib/util/formatters.co deleted file mode 100644 index f2c3b95..0000000 --- a/lib/util/formatters.co +++ /dev/null @@ -1,68 +0,0 @@ -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. - * @param {Boolean} [abbrev=true] Expand number suffixes if false. - * @returns {Object} Formatted number parts. - */ - numberFormatter: (n, digits=2, abbrev=true) -> - suffixes = do - if abbrev - [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] - else - [['Billion', 1000000000], ['Million', 1000000], ['', NaN]] - - for [suffix, d] of suffixes - 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, toString: -> - "#{@whole}#{@fraction}#{if abbrev then '' else ' '}#{@suffix}" - } - - numberFormatterHTML: (n, digits=2) -> - { whole, fraction, suffix } = _fmt._numberFormatter n, digits - # coco will trim the whitespace - " - #whole - #fraction - #suffix - " - - - - -module.exports = exports = _fmt diff --git a/lib/util/hashset.co b/lib/util/hashset.co deleted file mode 100644 index 0c99bef..0000000 --- a/lib/util/hashset.co +++ /dev/null @@ -1,418 +0,0 @@ -_ = require 'underscore' - - -/** - * A Set class, implemented using the `__id__` property on non-primitive objects it is passed. - * Arrays are hashed based on their contents. If an object lacks `__id__`, an exception will be - * thrown. This class does not keep values in sorted order. - * - * Underscore provides an easy way to generate unique IDs with the (surprise!) `_.uniqueId()` - * function. - * @see http://documentcloud.github.com/underscore/#uniqueId - * - * @class - */ -class HashSet - /** - * Objects by Id. - * @private - */ - _byId : {} - - /** - * Set contents. - * @private - */ - _o : [] - - /** - * Number of elements in the set. - * @property {Number} - */ - length : 0 - - - /** - * Accepts any number of collections to be added to the set. - * @constructor - */ - -> - @_byId = {} - @_o = [] - @update ...arguments if arguments.length - - - - /** - * Determine unique identifier for the given value. - * @private - * @returns {String} Id for this value. - */ - _getIdSafe : (v) -> - t = typeof v - - switch t - case 'undefined' - return 'u' - case 'boolean' 'string' 'number' - return "#{t.charAt 0}:#v" - if v is null - return 'n' - if '__id__' in v - return 'o:' + v.__id__ - if _.isArray v - return 'a:' + v.map @_getIdSafe, this .join ',' - - /** - * Determine unique identifier for the given value, throwing an exception otherwise. - * @private - * @returns {String} Id for this value. - */ - _getId : (v) -> - id = @_getIdSafe v - unless id? - throw new Error "HashSet elements must be hashable (#v)" - id - - - - /** - * Aliases: HashSet#has - * @param {Any} v Value to test. - * @returns {Boolean} Whether HashSet contains value. - */ - contains : (v) -> - @_getIdSafe(v) in @_byId - - - /** - * @private - * @returns {this} - */ - _addOne : (v) -> - id = @_getId v - unless id in @_byId - @_byId[id] = v - @_o.push(v) - @length = @_o.length - this - - - /** - * Add values to the HashSet. - * Aliases: HashSet#push HashSet#unshift - * @param {Any} values... Values to add. - * @returns {this} - */ - add : (...values) -> - _.each arguments, @_addOne, this - this - - /** - * @private - * @returns {this} - */ - _removeOne : (v) -> - id = @_getId v - if id in @_byId - delete @_byId[id] - @_o.splice @_o.indexOf(v), 1 - @length = @_o.length - this - - - /** - * Remove values from the HashSet. - * Aliases: HashSet#without - * @param {Any} values... Values to remove. - * @returns {this} - */ - remove : (...values) -> - _.each arguments, @_removeOne, this - this - - - /** - * Update this HashSet (in-place) with other collections. - * Aliases: HashSet#extend HashSet#concat - * @param {Array|Object} it... Collection to add. - * @returns {this} - */ - update : (vs) -> - _.each arguments, ~> _.each it, @_addOne, this - this - - - /** - * Remove and return an element from the set. - * Aliases: HashSet#shift - * @returns {Any} An element from the set. - */ - pop : -> - return unless @_o.length - v = @_o.shift() - id = @_getIdSafe v - delete @_byId[id] - return v - - - /** - * Returns but does not remove the an element from the set. - * @returns {Any} An element from the set. - */ - element : -> - @_o[0] - - - /** - * Clones the set, returning a new object. - * @returns {HashSet} - */ - clone : -> - new HashSet @_o - - - /** - * Removes all elements from the set. - * Aliases: HashSet#empty - * @returns {this} - */ - clear: -> - @_byId = {} - @_o = [] - @length = 0 - this - - - - ### Collection Operations - - /** - * Transforms the collection into a single value, front-to-back. - * Aliases: HashSet#inject HashSet#fold HashSet#foldl HashSet#foldr - * @param {Function} fn Reducer function. - * @param {Any} [acc] Starting accumulator value. - * @param {Object} [cxt=this] Context; defaults to this HashSet. - * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduce - * @returns {Any} - */ - reduce: (fn, acc, cxt) -> - _.reduce @_o, fn, acc, cxt or this - - /** - * Applies a function to each element. - * Aliases: HashSet#each - * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach - * @returns {this} - */ - forEach: (fn, cxt) -> - _.forEach @_o, fn, cxt or this - this - - /** - * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map - * @return {HashSet} A new HashSet of elements produced by applying the transform across each element. - */ - map: (fn, cxt) -> - new HashSet _.map @_o, fn, cxt or this - - - /** - * Aliases: HashSet#select - * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter - * @return {HashSet} A new HashSet of only the elements passing the filter. - */ - filter: (fn, cxt) -> - new HashSet _.filter @_o, fn, cxt or this - - /** - * Like `HashSet.filter()`, but instead keeps values for which the filter returns false. - * @see HashSet#filter - * @return {HashSet} A new HashSet of only the elements for which the filter returns false. - */ - reject: (fn, cxt) -> - new HashSet _.reject @_o, fn, cxt or this - - - /** - * Aliases: HashSet#any - * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some - * @return {Boolean} - */ - some: (fn, cxt) -> - _.some @_o, fn, cxt or this - - /** - * Aliases: HashSet#all - * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every - * @return {Boolean} - */ - every: (fn, cxt) -> - _.every @_o, fn, cxt or this - - - /** - * Iterates through the HashSet, returning the first value for which `fn` returns truth-y. - * Aliases: HashSet#detect - * @returns {Any} - */ - find: (fn, cxt) -> - _.find @_o, fn, cxt or this - - - /** - * @returns {Array} List of all values at property `prop`. - */ - pluck : (prop) -> - _.pluck @_o, prop - - - /** - * Invokes the named method on each element in the set, returning a list of the results. - * @param {String} methodName Name of the method on each element to call. - * @param {Any...} [args...] Optional arguments to call pass to method call. - * @returns {Array} List of results. - */ - invoke : (methodName) -> - _.invoke @_o, ...arguments - - - /** - * @returns {Array} List of the unique identifiers for each element of the set. - */ - keys : -> - _.keys @_byId - - /** - * Converts this HashSet to an Array. - * Aliases: HashSet#toArray - * @returns {Array} - */ - values : -> - @_o.slice() - - - - ### Comparators and HashSet Operations - - /** - * Tests if `a` is a Collection and has all elements in common with the set. - * Sets are equal if and only if their intersection has the same size as both sets. - * @param {Collection} a - * @returns {Boolean} - */ - equals : (a) -> - return false unless a - L = @_o.length - return L is a.length and L is @intersect(a).length - - - /** - * Tests if the set has no elements in common with `a`. - * Sets are disjoint if and only if their intersection is the empty set. - * @param {Collection} a - * @returns {Boolean} - */ - isDisjoint : (a) -> - return true unless a - return not _.some a, @contains, this - - - /** - * Test whether every element in the set is in `a`. - * @param {Collection} a - * @returns {Boolean} - */ - isSubset : (a) -> - return false unless a - A = _ if _.isArray a then a else _.values a - @every A.contains, A - - - /** - * Test whether every element in `a` is in the set. - * @param {Array|Object} a - * @returns {Boolean} - */ - isSuperset : (a) -> - return false unless a - _.every a, @contains, this - - - /** - * HashSet Intersection (A ^ B) - * Intersects this YArray with another collection, returning a new YArray. - * The membership test uses _(a).contains(), so it is possible to intersect collections of different types. - * For YArray and YObject, .contains() uses strict equality (is) via .indexOf(). - * - * @param {Array|Object} a Comparison collection. - * @returns {HashSet} A new YArray of all elements of {this} found in the supplied collection. - * - * @example - * foo = /foo/ - * A = [foo, 'A', 1, 2, 3, 'C', /foo/] - * B = [foo, 'B', 3, 'A', 1, /foo/] - * ins = _(A).intersect(B) - * ins.toString() is "HashSet([/foo/,A,1,3])"; # true - * ins.get(0) is foo; # true - */ - intersect : (a) -> - new HashSet _.intersect @_o, _.map arguments, _.values - - - /** - * HashSet Union (A v B) - * Aliases: HashSet#extend HashSet#concat - * @param {Array|Object} a Other collection(s). - * @returns {HashSet} A new HashSet of all elements of both collections, without duplicates. - */ - union : (a) -> - _.reduce arguments, ((out, it) -> out.update it), @clone() - - - /** - * HashSet Difference (A - B) - * @param {Array|Object} a Comparison collection(s). - * @returns {HashSet} A new HashSet of only elements of this HashSet not in supplied collection(s). - */ - difference : (a) -> - new HashSet _.difference @_o, _.map arguments, _.values - - - /** - * Symmetric Difference (A - B) v (B - A) - * @returns {HashSet} - */ - xor : (a) -> - a = _.values a - @difference a .union _.difference a, @_o - - - toString : -> - "HashSet([#{@_o}])" - - - -### Aliases - -pt = HashSet:: - -pt.push = pt.unshift = pt.add -pt.shift = pt.pop -pt.without = pt.remove -pt.empty = pt.clear -pt.has = pt.include = pt.contains - -pt.fold = pt.foldl = pt.foldr = pt.inject = pt.reduce -pt.each = pt.forEach -pt.select = pt.filter -pt.all = pt.every -pt.any = pt.some - -pt.detect = pt.find -pt.toArray = pt.values -pt.extend = pt.concat = pt.union - - -exports = module.exports = HashSet - diff --git a/lib/util/index.co b/lib/util/index.co deleted file mode 100644 index ddcacba..0000000 --- a/lib/util/index.co +++ /dev/null @@ -1,38 +0,0 @@ -_ = exports._ = require 'kraken/util/underscore' -op = exports.op = require 'kraken/util/op' - -# Root object -- `window` in the browser, `global` in Node. -root = exports.root = do -> this - -# Stub out console with empty methods -root.console or= _ <[ log info warn error dir table group groupCollapsed groupEnd ]> .synthesize -> [it, op.nop] - -### Extend jQuery with useful functions - -/** - * @returns {Object} Object of the data from the form, via `.serializeArray()`. - */ -root.jQuery?.fn.formData = -> - _.synthesize do - this.serializeArray() - -> [it.name, it.value] - -/** - * Invokes a jQuery method on each element, returning the array of the result. - * @returns {Array} Results. - */ -root.jQuery?.fn.invoke = (method, ...args) -> - for el, idx of this - jQuery(el)[method] ...args - - -exports import require 'kraken/util/event' - -backbone = exports.backbone = require 'kraken/util/backbone' -parser = exports.parser = require 'kraken/util/parser' -Cascade = exports.Cascade = require 'kraken/util/cascade' - -# HashSet = exports.HashSet = require 'kraken/util/hashset' -# BitString = exports.BitString = require 'kraken/util/bitstring' -# {crc32} = exports.{crc32} = require 'kraken/util/crc' - diff --git a/lib/util/op.co b/lib/util/op.co deleted file mode 100644 index e434d65..0000000 --- a/lib/util/op.co +++ /dev/null @@ -1,154 +0,0 @@ -DASH_PATTERN = /-/g - -STRIP_PAT = /(^\s*|\s*$)/g -strip = (s) -> - if s then s.replace STRIP_PAT, '' else s - -FALSEY = /^\s*(?:no|off|false)\s*$/i -parseBool = (s) -> - i = parseInt(s or 0) - !! if isNaN(i) then not FALSEY.test(s) else i - - - -module.exports = op = - I : (x) -> x - K : (k) -> -> k - nop : -> - kThis : -> this - kObject : -> {} - kArray : -> [] - - ### values - val : (def,o) -> o ? def - ok : (o) -> o? - notOk : (o) -> o!? - - ### manipulate function args/arity - first : (a) -> a - second : (_,a) -> a - nth : (n) -> - switch n - case 0 then op.first - case 1 then op.second - default -> arguments[n] - - # reverse the order of the first two args - flip : (fn) -> - (a, b) -> - arguments[0] = b - arguments[1] = a - fn.apply this, arguments - - # only pass n args - aritize : (fn, cxt, n) -> - [n, cxt] = [cxt, null] if arguments.length < 3 - -> fn.apply cxt ? this, [].slice.call(arguments, 0, n) - - # only pass the first argument - it : (fn, cxt) -> - -> fn.call cxt ? this, it - - - - ### reduce-ordered values & accessors - khas : (k,o) -> k in o - kget : (k,o) -> o[k] - defkget : (def,k,o) -> if k in o then o[k] else def - thisget : (k) -> this[k] - vkset : (o,v,k) -> o[k] = v if o and k?; o - - ### curry-ordered values & accessors - has : (o,k) -> k in o - get : (o,k) -> o[k] - getdef : (o,k,def) -> if k in o then o[k] else def - kvset : (o,k,v) -> o[k] = v if o and k?; o - thiskvset : (k,v) -> @[k] = v if k?; this - - prop : (k) -> (o) -> o[k] - method : (name, ...args) -> - (obj, ..._args) -> - obj[name] ...args.concat(_args) if obj?[name] - isK : (k) -> (v) -> v is k - - ### type coercion (w/ limited parameters for mapping) - parseBool : parseBool - toBool : parseBool - toInt : (v) -> parseInt v - toFloat : (v) -> parseFloat v - toStr : (v) -> String v - toRegExp : (v) -> new RegExp v - - toObject : (v) -> - if typeof v is 'string' and strip(v) - JSON.parse v - else - v - - toDate : (v) -> - return v if v!? or v instanceof Date - return new Date v if typeof v is 'number' - return new Date String(v).replace DASH_PATTERN, '/' - - - ### comparison - cmp : (x,y) -> if x < y then -1 else (if x > y then 1 else 0) - eq : (x,y) -> x == y - ne : (x,y) -> x != y - gt : (x,y) -> x > y - ge : (x,y) -> x >= y - lt : (x,y) -> x < y - le : (x,y) -> x <= y - - ### math - add : (x,y) -> x + y - sub : (x,y) -> x - y - mul : (x,y) -> x * y - div : (x,y) -> x / y - flrdiv : (x,y) -> Math.floor(x / y) - mod : (x,y) -> x % y - neg : (x) -> -x - log2 : (n) -> Math.log n / Math.LN2 - - - ### logic - is : (x,y) -> x is y - isnt : (x,y) -> x is not y - and : (x,y) -> x and y - or : (x,y) -> x or y - not : (x) -> not x - - ### bitwise - bitnot : (x) -> ~x - bitand : (x,y) -> x & y - bitor : (x,y) -> x | y - bitxor : (x,y) -> x ^ y - lshift : (x,y) -> x << y - rshift : (x,y) -> x >> y - # zrshift : (x,y) -> x >>> y - - ### binary - - # Binary representation of the number. - bin : (n) -> - do - s = (if n % 2 then '1' else '0') + (s or '') - n >>= 1 - while n - s - - # Number of bits needed to represent the absolute value of n. - binlen : (n) -> - bin Math.abs n .length - - # Returns a run of 1s of size n. - mask : (n) -> - (1 << n) - 1 - - # strings - chr : -> String.fromCharCode it - ord : -> String(it).charCodeAt 0 - encode : -> it and $ "
    #it
    " .html().replace /"/g, '"' - decode : -> it and $ "
    #it
    " .text() - strip : strip - diff --git a/lib/util/parser.co b/lib/util/parser.co deleted file mode 100644 index 1783703..0000000 --- a/lib/util/parser.co +++ /dev/null @@ -1,127 +0,0 @@ -_ = require 'kraken/util/underscore' -op = require 'kraken/util/op' -{ BaseModel, BaseList, BaseView, Mixin, -} = require 'kraken/base' - - -/** - * @namespace Parsers by type. - */ -Parsers = exports.Parsers = - - parseBoolean: (v) -> - op.toBool v - - parseInteger: (v) -> - r = op.toInt v - unless isNaN r then r else null - - parseFloat: (v) -> - r = op.toFloat v - unless isNaN r then r else null - - parseString: (v) -> - if v? then op.toStr v else null - - parseDate: (v) -> - if v then op.toDate v else null - - parseRegExp: (v) -> - if v then op.toRegExp v else null - - parseArray: (v) -> - if v then op.toObject v else null - - parseObject: (v) -> - if v then op.toObject v else null - - parseFunction: (v) -> - if v and _.startswith String(v), 'function' - try eval "(#v)" catch err then null - else - null - - -# Aliases -Parsers.parseNumber = Parsers.parseFloat - - -/** - * @class Methods for a class to select parsers by type reflection. - * @mixin - */ -class exports.ParserMixin extends Mixin - this:: import Parsers - - (target) -> - return Mixin.call ParserMixin, target - - - # XXX: So I'm meh about mixing in the Parsers dictionary. - # - # - Pros: mixing in `parseXXX()` methods makes it easy to - # override in the target class. - # - Cons: `parse()` is a Backbone method, which bit me once - # already (hence `parseValue()`), so conflicts aren't unlikely. - # - # Other ideas: - # - Parsers live at `@__parsers__`, and each instance gets its own clone - # -> Parser lookup uses a Cascade from the object. (Why not just use prototype, tho?) - - parseValue: (v, type) -> - @getParser(type)(v) - - getParser: (type='String') -> - # If this is a known type and we have a parser for it, return that - fn = @["parse#type"] - return fn if typeof fn is 'function' - - # Handle compound/optional types - # XXX: handle 'or' by returning an array of parsers? - type = _ String(type).toLowerCase() - for t of <[ Integer Float Number Boolean Object Array Function ]> - if type.startsWith t.toLowerCase() - return @["parse#t"] - @defaultParser or @parseString - - getParserFromExample: (v) -> - return null unless v? - type = typeof v - - if type is not 'object' - @getParser type - else if _.isArray v - @getParser 'Array' - else - @getParser 'Object' - - - - -/** - * @class Basic model which mixes in the ParserMixin. - * @extends BaseModel - * @borrows ParserMixin - */ -ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin.mix do - constructor: function ParsingModel then BaseModel ... - - -/** - * @class Basic collection which mixes in the ParserMixin. - * @extends BaseList - * @borrows ParserMixin - */ -ParsingList = exports.ParsingList = BaseList.extend ParserMixin.mix do - constructor: function ParsingList then BaseList ... - - -/** - * @class Basic view which mixes in the ParserMixin. - * @extends BaseView - * @borrows ParserMixin - */ -ParsingView = exports.ParsingView = BaseView.extend ParserMixin.mix do - constructor: function ParsingView then BaseView ... - - diff --git a/lib/util/timeseries/csv.co b/lib/util/timeseries/csv.co deleted file mode 100644 index e89053b..0000000 --- a/lib/util/timeseries/csv.co +++ /dev/null @@ -1,115 +0,0 @@ -_ = require 'kraken/util/underscore' -TimeSeriesData = require 'kraken/util/timeseries/timeseries' - - -DASH_PATTERN = /-/g -BLANK_LINE_PATTERN = /^(\s*)$/ -COMMENT_PATTERN = /\s*(#|\/\/).*$/ - -class CSVData extends TimeSeriesData - DEFAULT_OPTIONS : - colSep : ',' - rowSep : '\n' - defaultType : 'float' - customBars : false - customSep : ';' - errorBars : false - fractions : false - fractionSep : '/' - skipBlankLines : true - blankLinePat : BLANK_LINE_PATTERN - removeCommentedText : true - commentPat : COMMENT_PATTERN - replaceMissing : false - replaceMissingValue : 0 - replaceNaN : false - replaceNaNValue : 0 - padRows : false - padRowsValue : 0 - - - (data, opts) -> - super ... - - - /* * * * CSV Parsing * * * */ - - parseNumber: (s) -> - parseFloat s - - parseHiLo: (s) -> - s.split @options.customBars .map @parseNumber, this - - parseFraction: (s) -> - s.split @options.fractionSep .map @parseNumber, this - - parseDate: (s) -> - new Date s.replace DASH_PATTERN, '/' - - - /** - * Parses and imports a CSV string. - * - * @private - * @returns {this} - */ - parseData: (@rawData) -> - return this if typeof rawData is not 'string' - o = @options - - lines = rawData.split o.rowSep - return [] unless lines.length - first = lines[0] - - # Use the default delimiter or fall back to a tab if that makes sense. - delim = o.colSep - if first.indexOf(delim) is -1 and first.indexOf('\t') >= 0 - delim = '\t' - - rows = @rows = [] - @columns = [] - - parser = @parseNumber - parser = @parseHiLo if o.customBars - parser = @parseFraction if o.fractions - - hasHeaders = @labels.length is not 0 - for line, i of lines - line .= replace o.commentPat, '' if o.removeCommentedText - continue if o.skipBlankLines and (line.length is 0 or o.blankLinePat.test line) - - cols = line.split delim - unless hasHeaders - hasHeaders = true - @labels = cols.map -> _.strip it - continue - - continue unless cols.length > 1 - date = @parseDate cols.shift() - fields = cols.map parser, this - if o.errorBars - fields = fields.reduce do - (acc, v) -> - last = acc[acc.length-1] - unless last and last.length < 2 - acc.push last = [] - last.push v - acc - [] - - fields.unshift date - rows.push fields - fields.forEach (v, idx) ~> - @columns.push [] unless @columns[idx] - @columns[idx].push v - - @untransformedRows = _.merge [], @rows - this - - - - - -module.exports = exports = CSVData - - diff --git a/lib/util/timeseries/index.co b/lib/util/timeseries/index.co deleted file mode 100644 index b583a6b..0000000 --- a/lib/util/timeseries/index.co +++ /dev/null @@ -1,2 +0,0 @@ -exports.TimeSeriesData = require 'kraken/util/timeseries/timeseries' -exports.CSVData = require 'kraken/util/timeseries/csv' diff --git a/lib/util/timeseries/timeseries.co b/lib/util/timeseries/timeseries.co deleted file mode 100644 index 8fad863..0000000 --- a/lib/util/timeseries/timeseries.co +++ /dev/null @@ -1,191 +0,0 @@ -_ = require 'kraken/util/underscore' - - - -/** - * @class Represents a collection of data columns aligned along a common timeline. - */ -class TimeSeriesData - DEFAULT_OPTIONS : {} - - options : {} - labels : [] - types : [] - - untransformedRows : null # row-oriented (untransformed) - rows : null # row-oriented - columns : null # column-oriented (includes date column) - dateColumn : null # only date column - dataColumns : null # column-oriented (excludes date column) - - - /** - * @constructor - */ - (data, opts) -> - unless typeof data is 'string' or _.isArray data - [opts, data] = [data, null] - @options = _.clone(@DEFAULT_OPTIONS) import (opts or {}) - - @transforms = [] - @labels = @options.labels or [] - @types = @options.types or [] - - @parseData that if data or @options.data - @rebuildDerived() - - - /* * * * TimeSeriesData interface * * * */ - - - /** - * @returns {Array} List of rows, each of which includes all columns. - */ - getData: -> - @data - - /** - * @returns {Array} List of all columns (including date column). - */ - getColumns: -> - @columns - - /** - * @returns {Array} The date column. - */ - getDateColumn: -> - @dateColumn - - /** - * @returns {Array} List of all columns except the date column. - */ - getDataColumns: -> - @dataColumns - - /** - * @returns {Array} List of column labels. - */ - getLabels: -> - @labels - - - /* * * * Parsing * * * */ - - /** - * Subclass and override to perform preprocessing of the data. - * @private - */ - parseData : (rawData) -> - this - - /** - * Rebuilds the row-oriented data matrix from the columns. - * @private - */ - rebuildData: -> - @rows = _.zip ...@columns - @rebuildDerived() - - /** - * Rebuilds the column-oriented data matrix from the columns. - * @private - */ - rebuildColumns: -> - @columns = _.zip ...@rows - @rebuildDerived() - - /** - * @private - */ - rebuildDerived: -> - while @transforms.length < @columns.length - @transforms.push [] - @dateColumn = @columns[0] - @dataColumns = @columns.slice(1) - this - - - - /* * * * Data Transformation * * * */ - - /** - * Applies the stack of transforms to the data. - * - * TODO: Apply transforms in @getData()? - * @private - * @returns {this} - */ - applyTransforms: -> - for fns, idx of @transforms - for [fn, ctx] of fns - @columns[idx] .= map fn, ctx - @rebuildData() - - /** - * Clears all transforms and restores the original data. - * @returns {this} - */ - clearTransforms: -> - @transforms = [] - @rows = _.merge [], @untransformedRows - @rebuildColumns() - - /** - * Add a data transform to the specified columns. The function is - * applied one-by-one (in column-major order), replacing the data - * with the mapped result. - * - * @param {Number|Array} indices List one or more column indices to map. Negative - * numbers are offset from the end of the columns list. - * @param {Function} fn Mapping function of the form: - * `(single_value, row_idx, column) -> new_value` - * @param {Object} [ctx=this] Execution context for the function. - * @returns {this} - */ - addTransform: (indices, fn, ctx=this) -> - num_cols = @columns.length - if typeof idx is 'function' - [ctx, fn, indices] = [fn, indices, null] - unless indices? - indices = _.range num_cols - unless _.isArray indices - indices = [indices] - for idx of indices - idx %= num_cols - idx += num_cols if idx < 0 - @transforms[idx].push [fn, ctx] - @applyTransforms() - - /** - * Add a data transform to all columns except the date column. The function - * is applied one-by-one (in column-major order), replacing the data - * with the mapped result. - * - * @param {Function} fn Mapping function of the form: - * `(single_value, row_idx, column) -> new_value` - * @param {Object} [ctx=this] Execution context for the function. - * @returns {this} - */ - addDataTransform: (fn, ctx=this) -> - @addTransform _.range(1, @columns.length), fn, ctx - - - - /* * * * Misc * * * */ - - /** - * @returns {Array} Deep copy of the data rows (including all columns). - */ - toJSON: -> - _.merge [], @getData() - - toString: -> - labels = @labels - .map -> "'#it'" - .join ', ' - "#{@..name or @..displayName}(#labels)" - - - -module.exports = exports = TimeSeriesData - diff --git a/lib/util/underscore/_functions.co b/lib/util/underscore/_functions.co deleted file mode 100644 index 7327961..0000000 --- a/lib/util/underscore/_functions.co +++ /dev/null @@ -1,220 +0,0 @@ -_ = require 'underscore' - - -slice = [].slice -hasOwn = {}.hasOwnProperty -objToString = {}.toString - -toArray = _.toArray - - - -decorate = (fn) -> - if not fn.__decorated__ - for name of _pet.FUNCTION_METHODS - m = _[name] - fn[name] = m.__methodized__ or methodize m - fn.__decorated__ = true - return fn - -methodize = (fn) -> - m = fn.__methodized__ - return m if m - - g = fn.__genericized__ - return g.__wraps__ if g and g.__wraps__ - - m = fn.__methodized__ = (args...) -> - args.unshift this - return fn.apply this, args - - m.__wraps__ = fn - return decorate m - - - -_pet = module.exports = \ - function pet (o, start=0, end=undefined) -> - if _.isArguments o - o = _.toArray o, start, end - - return decorate o if typeof o is 'function' - return _ o - -# function methods to be attached on call to _(fn) -_pet.FUNCTION_METHODS = [ - 'bind', 'bindAll', 'memoize', - 'delay', 'defer', 'throttle', 'debounce', 'once', 'after', - 'wrap', 'compose', - 'unwrap', 'partial', 'curry', 'flip', 'methodize', 'aritize', 'limit' -] - - -class2name = "Boolean Number String Function Array Date RegExp Object" - .split(" ") - .reduce ((class2name, name) -> - class2name[ "[object "+name+"]" ] = name - return class2name), {} - - -## Objects -_.mixin - - has: (o, v) -> - vals = if _.isArray(o) then o else _.values(o) - return vals.indexOf(v) is not -1 - - remove: (o, vs...) -> - if _.isArray(o) - _.each vs, (v) -> - idx = o.indexOf v - if idx is not -1 - o.splice idx, 1 - else - _.each o, (v, k) -> - if vs.indexOf(v) != -1 - delete o[k] - return o - - set: (o, key, value, def) -> - if o and key? and (value? or def?) - o[key] = value ? def - return o - - attr: (o, key, value, def) -> - return o if not o or key is undefined - - if _.isPlainObject key - return _.extend o, key - - if (value ? def) is not undefined - return _.set o, key, value, def - - return o[key] - - - -## Types -_.mixin - - basicTypeName: (o) -> - return if o is null then "null" else (class2name[objToString.call(o)] || "Object") - - isWindow: (o) -> - return o and typeof o is "object" and "setInterval" of o - - isPlainObject: (o) -> - # Must be an Object. - # Because of IE, we also have to check the presence of the constructor property. - # Make sure that DOM nodes and window objects don't pass through, as well - if not o or basicTypeName(o) is not "Object" or o.nodeType or _.isWindow(o) - return false - - # Not own constructor property? must be Object - C = o.constructor - if C and not hasOwn.call(o, "constructor") and not hasOwn.call(C.prototype, "isPrototypeOf") - return false - - # Own properties are enumerated firstly, so to speed up, - # if last one is own, then all properties are own. - for key in o - ; # semicolon **on new line** is required by coffeescript to denote empty statement. - - return key is void or hasOwn.call(o, key) - - -## Arrays -_.mixin - - toArray: (iterable, start=0, end=undefined) -> - _.slice toArray(iterable), start, end - - flatten: (A) -> - _.reduce do - slice.call(arguments) - (flat, v) -> - flat.concat( if _.isArray v then _.reduce(v, arguments.callee, []) else v ) - [] - - - -## Functions -_ofArity = _.memoize do - (n, limit) -> - args = ( '$'+i for i from 0 til n ).join(',') - name = ( if limit then 'limited' else 'artized' ) - apply_with = ( if limit then "[].slice.call(arguments, 0, #{n})" else 'arguments' ) - return eval " - (function #{name}(fn){ - var _fn = function(#{args}){ return fn.apply(this, #{apply_with}); }; - _fn.__wraps__ = fn; - return _(_fn); - })" - -_.mixin do - methodize - - unwrap: (fn) -> - (fn and _.isFunction(fn) and _.unwrap(fn.__wraps__)) or fn - - - partial: (fn, ...args) -> - partially = -> - fn.apply this, args.concat slice.call(arguments) - _ partially import { __wraps__:fn } - - - genericize: (fn) -> - return that if fn.__genericized__ - return that if fn.__methodized__?.__wraps__ - - fn.__genericized__ = (...args) -> - fn.apply args.shift(), args - - _ fn.__genericized__ import { __wraps__:fn } - - - curry: (fn, ...args) -> - return fn unless _.isFunction fn - return fn.apply this, args if fn.__curried__ - - L = fn.length or _.unwrap(fn).length - return fn.apply this, args if args.length >= L - - curried = -> - _args = args.concat slice.call(arguments) - return fn.apply this, _args if _args.length >= L - _args.unshift fn - _.curry.apply this, _args - - _ curried import - __wraps__ : fn - __curried__ : args - - - flip: (fn) -> - return that if fn.__flipped__ - - fn.__flipped__ = -> - [arguments[0], arguments[1]] = [arguments[1], arguments[0]] - fn ... - - _ fn.__flipped__ import { __wraps__:fn } - - - aritize: (fn, n) -> - return fn if fn.length is n - fn.__aritized__ or= {} - fn.__aritized__[n] or= _ofArity(n, false)(fn) - - - limit: (fn, n) -> - fn.__limited__ or= {} - fn.__limited__[n] or= _ofArity(n, true)(fn) - - - - - - -_.extend _pet, _ diff --git a/lib/util/underscore/array.co b/lib/util/underscore/array.co deleted file mode 100644 index 76e078a..0000000 --- a/lib/util/underscore/array.co +++ /dev/null @@ -1,48 +0,0 @@ -_ = require 'underscore' - -I = -> it -defined = (o) -> o? - -_array = do - /** - * Transforms an Array of tuples (two-element Arrays) into an Object, such that for each - * tuple [k, v]: - * result[k] = v if filter(v) - * @param {Array} o A collection. - * @param {Function} [filter=defined] Optional filter function. If omitted, will - * exclude `undefined` and `null` values. - * @return {Object} Transformed result. - */ - generate : (o, filter=defined) -> - _.reduce do - o - (acc, [k, v], idx) -> - if k and (not filter or filter(v, k)) - acc[k] = v - acc - {} - - /** - * As {@link _.generate}, but first transforms the collection using `fn`. - * @param {Array} o A collection. - * @param {Function} [fn=I] Transformation function. Defaults to the identity transform. - * @param {Function} [filter=defined] Optional filter function. If omitted, will - * exclude `undefined` and `null` values. - * @param {Object} [context=o] Function context. - * @return {Object} Transformed result. - */ - synthesize : (o, fn=I, filter=defined, context) -> - _array.generate _.map(o, fn, context), filter - - - /** - * Symmetric Difference - */ - xor : (a, b) -> - a = _.values a - b = _.values b - return _.union _.difference(a,b), _.difference(b,a) - - - -exports import _array diff --git a/lib/util/underscore/class.co b/lib/util/underscore/class.co deleted file mode 100644 index 1381cb4..0000000 --- a/lib/util/underscore/class.co +++ /dev/null @@ -1,53 +0,0 @@ -_ = require 'underscore' - - -_cls = - - /** - * @returns {Array} The list of all superclasses for this class - * or object. Typically does not include Object or Function due to - * the prototype's constructor being set by the subclass. - */ - getSuperClasses : function getSuperClasses(Cls) - return [] unless Cls - - if Cls.__superclass__ or Cls.superclass or Cls.__super__?.constructor - superclass = that unless that is Cls - unless superclass - Cls = Cls.constructor if typeof Cls is not 'function' - if Cls.__superclass__ or Cls.superclass or Cls.__super__?.constructor - superclass = that unless that is Cls - unless superclass then [] - else [superclass].concat getSuperClasses superclass - - /** - * Looks up an attribute on the prototype of each class in the class - * hierarchy. Values from Object or Function are not typically included -- - * see the note at `getSuperClasses()`. - * - * @param {Object} obj Object on which to reflect. - * @param {String} prop Property to nab. - * @returns {Array} List of the values, from closest parent to furthest. - */ - pluckSuper : (obj, prop) -> - return [] unless obj - _ _cls.getSuperClasses(obj) .chain() - .pluck 'prototype' - .pluck prop - .value() - - /** - * As `.pluckSuper()` but includes value of `prop` on passed `obj`. Values - * from Object or Function are not typically included -- see the note - * at `getSuperClasses()`. - * - * @returns {Array} List of the values, starting with the object's own - * value, and then moving from closest parent to furthest. - */ - pluckSuperAndSelf : (obj, prop) -> - return [] unless obj - [ obj[prop] ].concat _cls.pluckSuper(obj, prop) - - - -exports import _cls diff --git a/lib/util/underscore/function.co b/lib/util/underscore/function.co deleted file mode 100644 index a6e50b7..0000000 --- a/lib/util/underscore/function.co +++ /dev/null @@ -1,27 +0,0 @@ -_ = require 'underscore' - -_fn = - - /** - * Decorates a function so that its receiver (`this`) is always added as the - * first argument, followed by the call arguments. - * @returns {Function} - */ - methodize : (fn) -> - m = fn.__methodized__ - return m if m - - g = fn.__genericized__ - return that if g?.__wraps__ - - m = fn.__methodized__ = (...args) -> - args.unshift this - fn.apply this, args - - m.__wraps__ = fn - m - - - -exports import _fn - diff --git a/lib/util/underscore/index.co b/lib/util/underscore/index.co deleted file mode 100644 index f59c17f..0000000 --- a/lib/util/underscore/index.co +++ /dev/null @@ -1,29 +0,0 @@ -_ = require 'underscore' -_.str = require 'underscore.string' -_.mixin _.str.exports() - -_.mixin require 'kraken/util/underscore/function' -_.mixin require 'kraken/util/underscore/array' -_.mixin require 'kraken/util/underscore/object' -_.mixin require 'kraken/util/underscore/class' -_.mixin require 'kraken/util/underscore/kv' -_.mixin require 'kraken/util/underscore/string' - - -## Debug -_.dump = (o, label='dump', expanded=true) -> - if not _.isArray(o) and _.isObject(o) - if expanded - console.group label - else - console.groupCollapsed label - for k, v in o - console.log "#k:", v - console.groupEnd() - else - console.log label, o - o - - -module.exports = exports = _ - diff --git a/lib/util/underscore/kv.co b/lib/util/underscore/kv.co deleted file mode 100644 index 4eb5389..0000000 --- a/lib/util/underscore/kv.co +++ /dev/null @@ -1,60 +0,0 @@ -_ = require 'underscore' - - -_kv = do - - /** - * Transforms an object to a string of URL-encoded KV-pairs (aka "www-form-encoding"). - */ - toKV: (o, item_delim='&', kv_delim='=') -> - _.reduce do - o - (acc, v, k) -> - acc.push encodeURIComponent(k)+kv_delim+encodeURIComponent(v) if k - acc - [] - .join item_delim - - /** - * Restores an object from a string of URL-encoded KV-pairs (aka "www-form-encoding"). - */ - fromKV: (qs, item_delim='&', kv_delim='=') -> - _.reduce do - qs.split item_delim - (acc, pair) -> - idx = pair.indexOf kv_delim - if idx is not -1 - [k, v] = [pair.slice(0, idx), pair.slice(idx+1)] - else - [k, v] = [pair, ''] - acc[ decodeURIComponent k ] = decodeURIComponent v if k - acc - {} - - /** - * Copies and flattens a tree of sub-objects into namespaced keys on the parent object, such - * that `{ "foo":{ "bar":1 } }` becomes `{ "foo.bar":1 }`. - */ - collapseObject: (obj, parent={}, prefix='') -> - prefix += '.' if prefix - _.each obj, (v, k) -> - if _.isPlainObject v - _.collapseObject v, parent, prefix+k - else - parent[prefix+k] = v - parent - - /** - * Inverse of `.collapseObject()` -- copies and expands any dot-namespaced keys in the object, such - * that `{ "foo.bar":1 }` becomes `{ "foo":{ "bar":1 }}`. - */ - uncollapseObject: (obj) -> - _.reduce do - obj - (acc, v, k) -> - _.setNested acc, k, v, {+ensure} - acc - {} - - -exports import _kv diff --git a/lib/util/underscore/object.co b/lib/util/underscore/object.co deleted file mode 100644 index 9bc6e23..0000000 --- a/lib/util/underscore/object.co +++ /dev/null @@ -1,270 +0,0 @@ -_ = require 'underscore' - -getProto = Object.getPrototypeOf -OBJ_PROTO = Object.prototype -{ - hasOwnProperty : hasOwn - toString : objToString -} = {} - -/** - * Default options for delegate-accessor functions. - */ -DEFAULT_DELEGATE_OPTIONS = exports.DEFAULT_DELEGATE_OPTIONS = - getter : 'get' - setter : 'set' - deleter : 'unset' - -/** - * Tombstone for deleted, non-passthrough keys. - */ -TOMBSTONE = exports.TOMBSTONE = {} - -/** - * Default options for nested-accessor functions. - */ -DEFAULT_NESTED_OPTIONS = exports.DEFAULT_NESTED_OPTIONS = - {-ensure, tombstone:TOMBSTONE} import DEFAULT_DELEGATE_OPTIONS - - - -/** - * @namespace Functions for working with objects and object graphs. - */ -_obj = do - - - # isPlainObject : (o) -> - # !!( o and _.isObject(o) and OBJ_PROTO is getProto(o) ) - - /** - * @returns {Boolean} Whether value is a plain object or not. - */ - isPlainObject: (obj) -> - # Must be an Object. - # Because of IE, we also have to check the presence of the constructor property. - # Make sure that DOM nodes and window objects don't pass through, as well. - if not obj or objToString.call(obj) !== "[object Object]" or obj.nodeType or obj.setInterval - return false - - # Not own constructor property must be Object - return false if obj.constructor - and not hasOwn.call(obj, "constructor") - and not hasOwn.call(obj.constructor.prototype, "isPrototypeOf") - - # Own properties are enumerated firstly, so to speed up, - # if last one is own, then all properties are own. - for key in obj then ; - return key is void or hasOwn.call obj, key - - - /** - * In-place removal of a value from an Array or Object. - */ - remove: (obj, v) -> - values = [].slice.call arguments, 1 - if _.isArray(obj) or obj instanceof Array - for v of values - idx = obj.indexOf v - obj.splice idx, 1 if idx is not -1 - else - for k, v in obj - delete obj[k] if -1 is not values.indexOf v - obj - - - /** - * Converts the collection to a list of its items: - * - Objects become a list of `[key, value]` pairs. - * - Strings become a list of characters. - * - Arguments objects become an array. - * - Arrays are copied. - */ - items: (obj) -> - if _.isObject(obj) and not _.isArguments(obj) - _.map obj, (v, k) -> [k, v] - else - [].slice.call obj - - - - ### Delegating Accessors - - isMember: (obj, v) -> - values = _.unique [].slice.call arguments, 1 - common = _.intersection _.values(obj), values - _.isEqual values, common - - get: (obj, key, def, opts) -> - return unless obj? - getter = opts?.getter or 'get' - if typeof obj[getter] is 'function' - obj[getter] key, def, opts - else - if obj[key] is not void then obj[key] else def - - set: (obj, key, value, opts) -> - return unless obj? - if key? and _.isObject(key) - [values, opts] = [key, value] - else - values = { "#key": value } - - setter = opts?.setter or 'set' - if typeof obj[setter] is 'function' - for key, value in values - obj[setter] key, value, opts - else - for key, value in values - obj[key] = value - - obj - - unset: (obj, key, opts) -> - return unless obj? - deleter = opts?.deleter or 'unset' - if typeof obj[deleter] is 'function' - obj[deleter] key, opts - else - delete obj[key] - - - - - - ### Nested Acccessors - - /** - * Searches a heirarchical object for a given subkey specified in dotted-property syntax, - * respecting sub-object accessor-methods (e.g., 'get', 'set') if they exist. - * - * @param {Object} base The object to serve as the root of the property-chain. - * @param {Array|String} chain The property-chain to lookup. - * @param {Object} [opts] Options: - * @param {Boolean} [opts.ensure=false] If true, intermediate keys that are `null` or - * `undefined` will be filled in with a new empty object `{}`, ensuring the get will - * return valid metadata. - * @param {String} [opts.getter="get"] Name of the sub-object getter method use if it exists. - * @param {String} [opts.setter="set"] Name of the sub-object setter method use if it exists. - * @param {String} [opts.deleter="unset"] Name of the sub-object deleter method use if it exists. - * @param {Object} [opts.tombstone=TOMBSTONE] Sentinel value to be interpreted as no-passthrough, - * forcing the lookup to fail and return `undefined`. TODO: opts.returnTombstone - * @returns {undefined|Object} If found, the object is of the form - * `{ key: Qualified key name, obj: Parent object of key, val: Value at obj[key], opts: Options }`. - * Otherwise `undefined`. - */ - getNestedMeta : (obj, chain, opts) -> - chain = chain.split('.') if typeof chain is 'string' - len = chain.length - 1 - opts = _.clone(DEFAULT_NESTED_OPTIONS) import (opts or {}) - - _.reduce do - chain - (obj, key, idx) -> - return unless obj? - val = _.get obj, key, void, opts - - if val is opts.tombstone - return unless ops.ensure - val = void - - if idx is len - return { key, val, obj, opts } - if not val? and opts.ensure - val = {} - _.set obj, key, val, opts - val - obj - - /** - * Searches a heirarchical object for a given subkey specified in dotted-property syntax. - * @param {Object} obj The object to serve as the root of the property-chain. - * @param {Array|String} chain The property-chain to lookup. - * @param {Any} [def=undefined] Value to return if lookup fails. - * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`. - * @returns {null|Object} If found, returns the value, and otherwise `default`. - */ - getNested : (obj, chain, def, opts) -> - meta = _.getNestedMeta obj, chain, opts - return def if meta?.val is void - meta.val - - /** - * Searches a heirarchical object for a given subkey specified in - * dotted-property syntax, setting it with the provided value if found. - * @param {Object} obj The object to serve as the root of the property-chain. - * @param {Array|String} chain The property-chain to lookup. - * @param {Any} value The value to set. - * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`. - * @returns {undefined|Any} If found, returns the old value, and otherwise `undefined`. - */ - setNested : (obj, chain, value, opts) -> - return unless meta = _.getNestedMeta obj, chain, opts - {obj, key, val, opts} = meta - _.set obj, key, value, opts - val - - /** - * Searches a heirarchical object for a potentially-nested key and removes it. - * - * @param {Object} obj The root of the lookup chain. - * @param {String|Array} chain The chain of property-keys to navigate. - * Nested keys can be supplied as a dot-delimited string (e.g., `_.unsetNested(obj, 'user.name')`), - * or an array of strings, allowing for keys with dots (eg., - * `_.unsetNested(obj, ['products', 'by_price', '0.99'])`). - * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`. - * @returns {undefined|Any} The old value if found; otherwise `undefined`. - */ - unsetNested : (obj, chain, opts) -> - return unless meta = _.getNestedMeta obj, chain, opts - {obj, key, val, opts} = meta - _.unset obj, key, opts - val - - - /** - * Recursively merges together any number of donor objects into the target object. - * Modified from `jQuery.extend()`. - * - * @param {Object} target Target object of the merge. - * @param {Object} ...donors Donor objects. - * @returns {Object} - */ - merge: (target={}, ...donors) -> - # Handle case when target is a string or something (possible in deep copy) - unless typeof target is "object" or _.isFunction(target) - target = if _.isArray donors[0] then [] else {} - - for donor of donors - # Only deal with non-null/undefined values - continue unless donor? - - # Extend the base object - _.each donor, (value, key) -> - current = target[key] - - # Prevent never-ending loop - return if target is value - - # Recurse if we're merging plain objects or arrays - if value and (_.isPlainObject(value) or (valueIsArray = _.isArray(value))) - if valueIsArray - current = [] unless _.isArray current - else - current = {} unless current and typeof current is 'object' - - # Never move original objects, clone them - _.set target, key, _.merge(current, value) - - # Don't bring in undefined values - else if value is not void - _.set target, key, value - - # Return the modified object - target - - - - - -exports import _obj diff --git a/lib/util/underscore/string.co b/lib/util/underscore/string.co deleted file mode 100644 index 9b1ee6e..0000000 --- a/lib/util/underscore/string.co +++ /dev/null @@ -1,75 +0,0 @@ -_ = require 'underscore' -_str = require 'underscore.string' - -_string = do - - - - /** - * As _.str.chop but from the right. - */ - rchop : (s, step) -> - s = String s - i = s.length - step = Number step - out = [] - return [s] if step <= 0 - while i > 0 - out.unshift s.slice Math.max(0, i-step), i - i -= step - out - - - drop : (s, ...parts) -> - do - starting = s - for part of parts - s .= slice part.length if _str.startsWith s, part - s .= slice 0, s.length-part.length if _str.endsWith s, part - while s and s is not starting - s - - ldrop : (s, ...parts) -> - do - starting = s - for part of parts - s .= slice part.length if _str.startsWith s, part - while s and s is not starting - s - - rdrop : (s, ...parts) -> - do - starting = s - for part of parts - s .= slice 0, s.length-part.length if _str.endsWith s, part - while s and s is not starting - s - - # Converts to snake_case, concatenates the key-value pair (with '_'), normalizing _'s. - # If only a key is given, domize auto-curries and waits for a second argument. - domize : (key='', value='') -> - key = _str.trim _str.underscored(key), '_' - if arguments.length <= 1 - arguments.callee.bind this, key - else - "#{key}_#{_str.trim _str.underscored(value), '_'}" - - shortname: (s) -> - return s if s.length <= 6 - parts = _ s - .chain() - .underscored() - .trim '_' - .value() - .replace /_+/g, '_' - .split '_' - .map -> _.capitalize it.slice 0, 2 - return s if parts.length is 1 #and s.length <= 8 - parts.shift().toLowerCase() + parts.join('') - - -_string import do - dropLeft : _string.ldrop - dropRight : _string.rdrop - -exports import _string diff --git a/src/app.co b/src/app.co new file mode 100644 index 0000000..a4185b0 --- /dev/null +++ b/src/app.co @@ -0,0 +1,51 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' + + +/** + * @class Application view, automatically attaching to an existing element + * found at `appSelector`. + * @extends Backbone.View + */ +AppView = exports.AppView = Backbone.View.extend do # {{{ + appSelector : '#content .inner' + + + /** + * @constructor + */ + constructor: function AppView (options={}) + if typeof options is 'function' + @initialize = options + options = {} + else + @initialize = that if options.initialize + + @appSelector = that if options.appSelector + options.el or= jQuery @appSelector .0 + Backbone.View.call this, options + + jQuery ~> @render() + this + + /** + * Override to set up your app. This method may be passed + * as an option to the constructor. + */ + initialize: -> # stub + + /** + * Append subviews. + */ + render : -> + @$el.append @view.el if @view and not @view.$el.parent()?.length + + getClassName: -> + "#{@..name or @..displayName}" + + toString: -> + "#{@getClassName()}()" +# }}} + diff --git a/src/base/asset-manager.co b/src/base/asset-manager.co new file mode 100644 index 0000000..fa91012 --- /dev/null +++ b/src/base/asset-manager.co @@ -0,0 +1,43 @@ +{ _, op, +} = require 'kraken/util' +{ ReadyEmitter, +} = require 'kraken/util/event' + + + + +class AssetManager extends ReadyEmitter + # Map from key/url to data. + assets : null + + + /** + * @constructor + */ + -> + super ... + @assets = {} + + + + + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + load: -> + return this if @ready + proto = @constructor:: + jQuery.ajax do + url : @SPEC_URL + success : (spec) ~> + proto.spec = spec + proto.options_ordered = spec + proto.options = _.synthesize spec, -> [it.name, it] + proto.ready = true + @emit 'ready', this + error: ~> console.error "Error loading #{@typeName} spec! #it" + this + + + diff --git a/src/base/base-mixin.co b/src/base/base-mixin.co new file mode 100644 index 0000000..c6764cb --- /dev/null +++ b/src/base/base-mixin.co @@ -0,0 +1,221 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' + + + +BaseBackboneMixin = exports.BaseBackboneMixin = + + initialize: -> + @__apply_bind__() + + + ### Auto-Bound methods + + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */ + __bind__ : [] + + /** + * Applies the contents of `__bind__`. + */ + __apply_bind__: -> + names = _ @pluckSuperAndSelf '__bind__' .chain().flatten().compact().unique().value() + _.bindAll this, ...names if names.length + + + + + ### Events + + /** + * Whether we're ready. + * @type Boolean + */ + ready : false + + + /** + * Triggers the 'ready' event if it has not yet been triggered. + * Subsequent listeners added on this event will be auto-triggered. + * @returns {this} + */ + triggerReady: (lock='ready', event='ready') -> + return this if @[lock] + @[lock] = true + @trigger event, this + this + + /** + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @returns {this} + */ + resetReady: (lock='ready', event='ready') -> + return this unless @[lock] + @[lock] = false + @trigger "#event-reset", this + this + + /** + * Wrap {@link Backbone.Event#on} registration to handle registrations + * on 'ready' after we've broadcast the event. Handler will always still + * be registered, however, in case the emitter is reset. + * + * @param {String} events Space-separated events for which to register. + * @param {Function} callback + * @param {Object} [context] + * @returns {this} + */ + on: (events, callback, context=this) -> + return this if not callback + Backbone.Events.on ... + if @ready and _.contains events.split(/\s+/), 'ready' + callback.call context, this + this + + makeHandlersForCallback: (cb) -> + success : ~> cb.call this, [null].concat arguments + error : ~> cb.call this, it + + + + + ### Synchronization + + /** + * Count of outstanding tasks. + * @type Number + */ + waitingOn : 0 + + + /** + * Increment the waiting task counter. + * @returns {this} + */ + wait: -> + count = @waitingOn + @waitingOn += 1 + # console.log "#this.wait! #count --> #{@waitingOn}" + # console.trace() + @trigger('start-waiting', this) if count is 0 and @waitingOn > 0 + this + + /** + * Decrement the waiting task counter. + * @returns {this} + */ + unwait: -> + count = @waitingOn + @waitingOn -= 1 + # console.warn "#this.unwait! #{@waitingOn} < 0" if @waitingOn < 0 + # console.log "#this.unwait! #count --> #{@waitingOn}" + # console.trace() + @trigger('stop-waiting', this) if @waitingOn is 0 and count > 0 + this + + /** + * @param {Function} fn Function to wrap. + * @returns {Function} A function wrapping the passed function with a call + * to `unwait()`, then delegating with current context and arguments. + */ + unwaitAnd: (fn) -> + self = this + -> + # console.log "#self.unwaitAnd( function #{fn.name or fn.displayName}() )" + # console.trace() + self.unwait(); fn ... + + + + ### + + getClassName: -> + "#{@..name or @..displayName}" + + toString: -> + "#{@getClassName()}()" + + + + +/** + * @class Base mixin class. Extend this to create a new mixin, attaching the + * donor methods as you would instance methods. + * + * To mingle your mixin with another class or object: + * + * class MyMixin extends Mixin + * foo: -> "foo!" + * + * # Mix into an object... + * o = MyMixin.mix { bar:1 } + * + * # Mix into a Coco class... + * class Bar + * MyMixin.mix this + * bar : 1 + * + */ +class exports.Mixin + + /** + * Mixes this mixin into the target. If `target` is not a class, a new + * object will be returned which inherits from the mixin. + */ + @mix = (target) -> + return that unless target + + MixinClass = Mixin + MixinClass = @constructor if this instanceof Mixin + MixinClass = this if this instanceof Function + + if typeof target is 'function' + target:: import MixinClass:: + else + target = _.clone(MixinClass::) import target + + (target.__mixins__ or= []).push MixinClass + target + + /** + * Coco metaprogramming hook to propagate class properties and methods. + */ + @extended = (SubClass) -> + SuperClass = this + for own k, v in SuperClass + SubClass[k] = v unless SubClass[k] + SubClass + + + +# /** +# * @returns {Function} Function which takes a target object or class, +# * mixes the MixinClass into it, and then returns it. If the target is +# * not a class, a new object will be returned which inherits from the mixin. +# */ +# makeMixer = exports.makeMixer = (MixinClass) -> +# mixinBody = if typeof MixinClass is 'function' then MixinClass:: else MixinClass +# mixinMixer = (target) -> +# if typeof target is 'function' +# target:: import mixinBody +# else +# target = _.clone(mixinBody) import target +# target +# +# mixinBase = exports.mixinBase = makeMixer BaseBackboneMixin + + +/** + * Mixes BaseBackboneMixin into another object or prototype. + * @returns {Object} The merged prototype object. + */ +mixinBase = exports.mixinBase = (...bodies) -> + _.extend _.clone(BaseBackboneMixin), ...bodies + + + diff --git a/src/base/base-model.co b/src/base/base-model.co new file mode 100644 index 0000000..54b725e --- /dev/null +++ b/src/base/base-model.co @@ -0,0 +1,252 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' +{ BaseBackboneMixin, mixinBase, +} = require 'kraken/base/base-mixin' + + + +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ +BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ + + constructor : function BaseModel + @__class__ = @constructor + @__superclass__ = @..__super__.constructor + @waitingOn = 0 + Backbone.Model ... + # @..trigger 'create', this + + + + + ### Accessors + + url: -> + "#{@urlRoot}/#{@get('id')}.json" + + has: (key) -> + @get(key)? + + get: (key) -> + _.getNested @attributes, key + + # TODO: nested sets, handling events + + # set: (key, value, opts) -> + # if _.isObject(key) and key? + # [values, opts] = [key, value] + # else + # values = { "#key": value } + # + # # TODO: Validation + # @_changed or= {} + # + # for key, value in values + # if _.str.contains key, '.' + # _.setNested @attributes, key, value, opts + # else + # Backbone.Model::set.call this, key, value, opts + # + # this + # + # unset : (key, opts) -> + # + + + ### 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 successfully. + * @param {String} [opts.errorEvent='load-error'] Event which signals loading has completed but failed. + * @param {Boolean} [opts.force=false] If true, reset ready state if we're ready before proceeding. + * @param {Boolean} [opts.readyIfError=false] If true, move fire the ready event when loading completes, even if it failed. + * @returns {this} + */ + loader: (opts={}) -> + opts = { + -force + -readyIfError + startEvent : 'load' + completeEvent : 'load-success' + errorEvent : 'load-error' + ...opts + } + @resetReady() if opts.force + throw new Error('You must specify a `start` function to start loading!') unless opts.start + return this if @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() + @once opts.errorEvent, ~> + # console.log "#{this}.onLoadError()" + @loading = false + @unwait() # terminates the `load` wait + @trigger 'load-error', this unless opts.errorEvent is 'load-error' + @triggerReady() if opts.readyIfError + + # 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 + + serialize: (v) -> + # if v!? + # v = '' + if _.isBoolean v + v = Number v + else if _.isObject v + v = JSON.stringify v + String v + + /** + * Like `.toJSON()` in that it should return a plain object with no functions, + * but for the purpose of `.toKV()`, allowing you to customize the values + * included and keys used. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepFunctions=false] If false, functions will be omitted from the result. + * @returns {Object} + */ + toKVPairs: (opts={}) -> + opts = {-keepFunctions, ...opts} + kvo = _.collapseObject @toJSON() + for k, v in kvo + kvo[k] = @serialize v if opts.keepFunctions or typeof v is not 'function' + kvo + + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */ + toKV: (item_delim='&', kv_delim='=') -> + _.toKV @toKVPairs(), item_delim, kv_delim + + /** + * @returns {String} URL identifying this model. + */ + toURL: -> + "?#{@toKV ...}" + + toString: -> + "#{@getClassName()}(cid=#{@cid}, id=#{@id})" + + +# Class Methods +BaseModel import do + /** + * Factory method which constructs an instance of this model from a string of KV-pairs. + * This is a class method inherited by models which extend {BaseModel}. + * @static + * @param {String|Object} o Serialized KV-pairs (or a plain object). + * @returns {BaseModel} An instance of this model. + */ + fromKV: (o, item_delim='&', kv_delim='=') -> + o = _.fromKV o, item_delim, kv_delim if typeof o is 'string' + Cls = if typeof this is 'function' then this else this.constructor + new Cls _.uncollapseObject o + +# }}} + + + +/** + * @class Base collection, extending Backbone.Collection, used by scaffold and others. + * @extends Backbone.Collection + */ +BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{ + + constructor : function BaseList + @__class__ = @constructor + @__superclass__ = @..__super__.constructor + @waitingOn = 0 + Backbone.Collection ... + # @trigger 'create', this + + + getIds: -> + @models.map -> it.id or it.get('id') or it.cid + + + ### Serialization + + toKVPairs: -> + _.collapseObject @toJSON() + + toKV: (item_delim='&', kv_delim='=') -> + _.toKV @toKVPairs(), item_delim, kv_delim + + toURL: (item_delim='&', kv_delim='=') -> + "?#{@toKV ...}" + + toString: -> + "#{@getClassName()}[#{@length}]" + + toStringWithIds: -> + modelIds = @models + .map -> "\"#{it.id ? it.cid}\"" + .join ', ' + "#{@getClassName()}[#{@length}](#modelIds)" +# }}} + + diff --git a/src/base/base-view.co b/src/base/base-view.co new file mode 100644 index 0000000..18646a5 --- /dev/null +++ b/src/base/base-view.co @@ -0,0 +1,293 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' +{ BaseBackboneMixin, mixinBase, +} = require 'kraken/base/base-mixin' +{ BaseModel, +} = require 'kraken/base/base-mixin' +{ DataBinding, +} = require 'kraken/base/data-binding' + + + +/** + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View + */ +BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ + tagName : 'section' + model : BaseModel + + /** + * Method-name called by `onReturnKeypress` when used as an event-handler. + * @type String + */ + callOnReturnKeypress: null + + + /** + * Parent view of this view. + * @type BaseView + */ + parent : null + + /** + * Array of [view, selector]-pairs. + * @type Array<[BaseView, String]> + */ + subviews : [] + + /** + * Whether this view has been added to the DOM. + * @type Boolean + */ + isAttached: false + + + + constructor : function BaseView + @__class__ = @constructor + @__superclass__ = @..__super__.constructor + @waitingOn = 0 + @subviews = new ViewList + @onReturnKeypress = _.debounce @onReturnKeypress.bind(this), 50 + Backbone.View ... + @trigger 'create', this + + initialize: -> + @__apply_bind__() + + @setModel @model + @build() + @$el.on 'form submit', -> it.preventDefault() + + setModel: (model) -> + if @model + @model.off 'change', @render, this + @model.off 'destroy', @remove, this + delete @model.view + data = @$el.data() + delete data.model + delete data.view + if @model = model + @model.view = this + @$el.data { @model, view:this } + @model.on 'change', @render, this + @model.on 'destroy', @remove, this + @trigger 'change:model', this, model + model + + + + ### Subviews {{{ + + setParent: (parent) -> + [old_parent, @parent] = [@parent, parent] + @trigger 'parent', this, parent, old_parent + this + + unsetParent: -> + [old_parent, @parent] = [@parent, null] + @trigger 'unparent', this, old_parent + this + + + addSubview: (view) -> + @removeSubview view + @subviews.push view + view.setParent this + view + + removeSubview: (view) -> + if @hasSubview view + view.remove() + @subviews.remove view + view.unsetParent() + view + + hasSubview: (view) -> + @subviews.contains view + + invokeSubviews: -> + @subviews.invoke ...arguments + + removeAllSubviews: -> + @subviews.forEach @removeSubview, this + # @subviews = new ViewList + this + + + + ### }}} + ### DOM Utilities {{{ + + attach: (el) -> + # @undelegateEvents() + @$el.appendTo el + # only trigger the event the first time + return this if @isAttached + @isAttached = true + _.delay do + ~> # have to let DOM settle to ensure elements can be found + @delegateEvents() + @trigger 'attach', this + 50 + this + + remove : -> + # @undelegateEvents() + @$el.remove() + return this unless @isAttached + @isAttached = false + @trigger 'unattach', this + this + + clear : -> + @remove() + @model.destroy() + @trigger 'clear', this + this + + hide : -> @$el.hide(); @trigger('hide', this); this + show : -> @$el.show(); @trigger('show', this); this + + /** + * Attach each subview to its bind-point. + * @returns {this} + */ + attachSubviews: -> + bps = @getOwnSubviewBindPoints() + if @subviews.length and not bps.length + console.warn "#this.attachSubviews(): no subview bind-points found!" + return this + for view of @subviews + if bp = @findSubviewBindPoint view, bps + view.attach bp + else + console.warn "#this.attachSubviews(): Unable to find bind-point for #view!" + this + + /** + * Finds all subview bind-points under this view's element, but not under + * the view element of any subview. + * @returns {jQuery|undefined} + */ + getOwnSubviewBindPoints: -> + @$ '[data-subview]' .not @$ '[data-subview] [data-subview]' + + /** + * Find the matching subview bind-point for the given view. + */ + findSubviewBindPoint: (view, bind_points) -> + bind_points or= @getOwnSubviewBindPoints() + + # check if any bindpoint specifies this subview by id + if view.id + bp = bind_points.filter "[data-subview$=':#{view.id}']" + return bp.eq 0 if bp.length + + # Find all elements that specify this type as the subview type + bp = bind_points.filter "[data-subview='#{view.getClassName()}']" + return bp.eq 0 if bp.length + + + ### }}} + ### Rendering Chain {{{ + + toTemplateLocals: -> + @model.toJSON() + + $template: -> + $ @template { _, op, @model, view:this, ...@toTemplateLocals() } + + build: -> + return this unless @template + outer = @$template() + @$el.html outer.html() + .attr do + id : outer.attr 'id' + class : outer.attr 'class' + @attachSubviews() + @isBuilt = true + this + + render: -> + @wait() + if @isBuilt + @update() + else + @build() + @renderSubviews() + @trigger 'render', this + @unwait() + this + + renderSubviews: -> + @attachSubviews() + @subviews.invoke 'render' + this + + update: -> + new DataBinding this .update locals = @toTemplateLocals() + @trigger 'update', this, locals + this + + + /* * * * Events * * * */ + + bubbleEventDown: (evt) -> + @invokeSubviews 'trigger', ...arguments + this + + redispatch: (evt, ...args) -> + @trigger evt, this, ...args + this + + onlyOnReturn: (fn, ...args) -> + fn = _.debounce fn.bind(this), 50 + (evt) ~> fn.apply this, args if evt.keyCode is 13 + + /** + * Call a delegate on keypress == the return key. + * @returns {Function} Keypress event handler. + */ + onReturnKeypress: (evt) -> + fn = this[@callOnReturnKeypress] if @callOnReturnKeypress + fn.call this if fn and evt.keyCode is 13 + + toString : -> + "#{@getClassName()}(model=#{@model})" + + +# Proxy model methods +<[ get set unset toJSON toKV toURL ]> + .forEach (methodname) -> + BaseView::[methodname] = -> @model[methodname].apply @model, arguments + +# }}} + + + +class exports.ViewList extends Array + + (views=[]) -> + super ... + + extend: (views) -> + _.each views, ~> @push it + this + + findByModel: (model) -> + @find -> it.model is model + + toString: -> + contents = if @length then "\"#{@join '","'}\"" else '' + "ViewList[#{@length}](#contents)" + + +<[ each contains invoke pluck find remove compact flatten without union intersection difference unique uniq ]> + .forEach (methodname) -> + ViewList::[methodname] = -> _[methodname].call _, this, ...arguments + + diff --git a/src/base/base.co b/src/base/base.co new file mode 100644 index 0000000..5d3f699 --- /dev/null +++ b/src/base/base.co @@ -0,0 +1,74 @@ +{EventEmitter} = require 'events' +EventEmitter::off = EventEmitter::removeListener +EventEmitter::trigger = EventEmitter::emit + +{ _, op +} = require 'kraken/util' + + + +/** + * @class Eventful base class. + * @extends EventEmitter + */ +class Base extends EventEmitter + + /** + * After the super chain has exhausted (but not necessarily at the end + * of init -- it depends on when you super()), Base will publish a 'new' + * event on the instance's class, allowing anyone to subscribe to + * notifications about new objects. + * @constructor + */ + -> + @__class__ = @.. + @__superclass__ = @..superclass + @__apply_bind__() + super() + @__class__.emit 'new', this + + + ### Auto-Bound methods + + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */ + __bind__ : [] + + /** + * Applies the contents of `__bind__`. + */ + __apply_bind__: -> + names = _ @pluckSuperAndSelf '__bind__' .chain().flatten().compact().unique().value() + _.bindAll this, ...names if names.length + + + + getClassName: -> + "#{@..name or @..displayName}" + + toString: -> + "#{@getClassName()}()" + + + + ### Class Methods + + @extended = (Subclass) -> + # copy over all class methods, including this + for own k, v in this + Subclass[k] = v if typeof v is 'function' + Subclass.__super__ = @:: + Subclass + + + + +for k of <[ getSuperClasses pluckSuper pluckSuperAndSelf ]> + Base[k] = Base::[k] = _.methodize _[k] + +Base import EventEmitter:: + + +module.exports = Base diff --git a/src/base/cascading-model.co b/src/base/cascading-model.co new file mode 100644 index 0000000..7582621 --- /dev/null +++ b/src/base/cascading-model.co @@ -0,0 +1,57 @@ +{ _, op, +} = require 'kraken/util' +{ BaseModel, BaseList, +} = require 'kraken/base/base-model' + +Cascade = require 'kraken/util/cascade' + + + +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend do # {{{ + /** + * The lookup cascade. + * @type Cascade + */ + cascade : null + + + constructor: function CascadingModel (attributes={}, opts) + @cascade = new Cascade attributes + BaseModel.call this, attributes, opts + + initialize: -> + BaseModel::initialize ... + + + /** + * Recursively look up a (potenitally nested) attribute in the lookup chain. + * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys). + * @returns {*} + */ + get: (key) -> + @cascade.get key + + + toJSON: (opts={}) -> + opts = {-collapseCascade, ...opts} + if opts.collapseCascade + @cascade.collapse() + else + BaseModel::toJSON ... + + + +# Proxy Cascade methods +<[ + addLookup removeLookup popLookup shiftLookup unshiftLookup + isOwnProperty isOwnValue isInheritedValue isModifiedValue +]>.forEach (methodname) -> + CascadingModel::[methodname] = -> @cascade[methodname].apply @cascade, arguments + +# }}} + + + diff --git a/src/base/data-binding.co b/src/base/data-binding.co new file mode 100644 index 0000000..4b2fa58 --- /dev/null +++ b/src/base/data-binding.co @@ -0,0 +1,67 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' + + +class exports.DataBinding + + data : null + context : null + el : null + $el : null + bindPoints: null + + + + (el, @context=el) -> + if el instanceof Backbone.View + el = el.$el + @$el = $ el + @el = @$el.get 0 + + # Find all bind-points under this element, but not under a subview + @bindPoints = @$ '[data-bind], [name]' .not @$('[data-subview]').find('[data-bind], [name]') + + $: (sel) -> + @$el.find sel + + serialize: -> + it + + update: (@data) -> + for key, val in _.collapseObject(@data) + @updateBinding key, val + this + + updateBinding: (key, val) -> + # if val and _.isPlainObject val + # for k, v in val + # @updateBinding "#key.#k", v + # return this + + if bp = @findDataBindPoint key + if _.isFunction val + val.call @context, val, key, bp, @data + else if bp.is 'input:checkbox' + bp.attr 'checked', !!val + else if bp.is 'input, textarea' + bp.val @serialize val + else + if op.toBool bp.data 'data-bind-escape' + bp.text @serialize val + else + bp.html @serialize val + else + false and console.warn "#this.updateBinding(): Unable to find data bind-point for #key=#val!" + this + + findDataBindPoint: (key) -> + bp = @bindPoints.filter "[name='#key'], [data-bind='#key']" + return bp.eq(0) if bp.length + + + + + + diff --git a/src/base/index.co b/src/base/index.co new file mode 100644 index 0000000..17b6726 --- /dev/null +++ b/src/base/index.co @@ -0,0 +1,9 @@ +exports.Base = require 'kraken/base/base' +mixins = require 'kraken/base/base-mixin' +models = require 'kraken/base/base-model' +views = require 'kraken/base/base-view' +cache = require 'kraken/base/model-cache' +cascading = require 'kraken/base/cascading-model' +data_binding = require 'kraken/base/data-binding' +exports import mixins import models import views \ + import cache import cascading import data_binding diff --git a/src/base/model-cache.co b/src/base/model-cache.co new file mode 100644 index 0000000..f636a7d --- /dev/null +++ b/src/base/model-cache.co @@ -0,0 +1,196 @@ +_ = require 'underscore' +Seq = require 'seq' + +{ReadyEmitter} = require 'kraken/util/event' + + +# TODO: Bubble events to decorated emitters +# TODO: Automatically create a cache for any class that extends BaseModel +/** + * @class Caches models and provides static lookups by ID. + */ +class exports.ModelCache extends ReadyEmitter + /** + * @see ReadyEmitter#readyEventName + * @private + * @constant + * @type String + */ + readyEventName : 'cache-ready' + + /** + * Default options. + * @private + * @constant + * @type Object + */ + DEFAULT_OPTIONS: + ready : true + cache : null + create : null + ModelType : null + + /** + * @private + * @type Object + */ + options : null + + /** + * Type we're caching (presumably extending `Backbone.Model`), used to create new + * instances unless a `create` function was provided in options. + * @private + * @type Class + */ + ModelType : null + + /** + * Collection holding the cached Models. + * @private + * @type Backbone.Collection + */ + cache : null + + + + /** + * @constructor + * @param {Class} [ModelType] Type of cached object (presumably extending + * `Backbone.Model`), used to create new instances unless `options.create` + * is provided. + * @param {Object} [options] Options: + * @param {Boolean} [options.ready=true] Starting `ready` state. If false, + * the cache will queue lookup calls until `triggerReady()` is called. + * @param {Class} [options.cache=new Backbone.Collection] + * The backing data-structure for the cache. If omitted, we'll use a new + * `Backbone.Collection`, but really, anything with a `get(id)` method for + * model lookup will work here. + * @param {Function} [options.create] A function called when a new Model + * object is needed, being passed the new model ID. + * @param {Class} [options.ModelType] Type of cached object + * (presumably extending `Backbone.Model`), used to create new instances + * unless `options.create` is provided. + */ + (ModelType, options) -> + unless _.isFunction ModelType + [options, ModelType] = [ModelType or {}, null] + @options = {...@DEFAULT_OPTIONS, ...options} + + @cache = @options.cache or new Backbone.Collection + + @ModelType = ModelType or @options.ModelType + @createModel = that if @options.create + + @ready = !!@options.ready + @decorate @ModelType if @ModelType + + + /** + * Called when a new Model object is needed, being passed the new model ID. + * Uses the supplied `ModelType`; overriden by `options.create` if provided. + * + * @param {String} id The model ID to create. + * @returns {Model} Created model. + */ + createModel: (id) -> + new @ModelType {id} + + /** + * Registers a model with the cache. If a model by this ID already exists + * in the cache, it will be removed and this one will take its place. + * + * Fires an `add` event. + * + * @param {Model} model The model. + * @returns {Model} The model. + */ + register: (model) -> + # console.log "ModelCache(#{@CACHE}).register(#{model.id or model.get('id')})", model + if @cache.contains model + @cache.remove model, {+silent} + @cache.add model + @trigger 'add', this, model + model + + /** + * Synchronously check if a model is in the cache, returning it if so. + * + * @param {String} id The model ID to get. + * @returns {Model} + */ + get: (id) -> + @cache.get id + + /** + * Asynchronously look up any number of models, requesting them from the + * server if not already known to the cache. + * + * @param {String|Array} ids List of model IDs to lookup. + * @param {Function} cb Callback of the form `(err, models)`, + * where `err` will be null on success and `models` will be an Array + * of model objects. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + lookupAll: (ids, cb, cxt=this) -> + ids = [ids] unless _.isArray ids + # console.log "ModelCache(#{@cache}).lookup([#ids], #{typeof cb})" + + unless @ready + @on 'cache-ready', ~> + @off 'cache-ready', arguments.callee + @lookupAll ids, cb, cxt + return this + + Seq ids + .parMap_ (next, id) ~> + return next.ok(that) if @cache.get id + @register @createModel id + .on 'ready', -> next.ok it + .load() + .unflatten() + .seq (models) -> + cb.call cxt, null, models + .catch (err) -> + cb.call cxt, err + this + + /** + * Looks up a model, requesting it from the server if it is not already + * known to the cache. + * + * @param {String|Array} id Model ID to lookup. + * @param {Function} cb Callback of the form `(err, model)`, + * where `err` will be null on success and `model` will be the + * model object. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + lookup: (id, cb, cxt=this) -> + @lookupAll [id], (err, models) -> + if err then cb.call cxt, err + else cb.call cxt, null, models[0] + + /** + * Decorate an object with the cache methods: + * - register + * - get + * - lookup + * - lookupAll + * + * This is automatically called on `ModelType` if supplied. + * + * @param {Object} obj Object to decorate. + * @returns {obj} The supplied object. + */ + decorate: (obj) -> + obj.__cache__ = this + # Bind the ModelCache methods to the class + for m of <[ register get lookup lookupAll ]> + obj[m] = @[m].bind this + obj + + toString: -> + "#{@..displayName or @..name}(cache=#{@cache})" + + diff --git a/src/base/scaffold/index.co b/src/base/scaffold/index.co new file mode 100644 index 0000000..6524ae6 --- /dev/null +++ b/src/base/scaffold/index.co @@ -0,0 +1,3 @@ +models = require 'kraken/base/scaffold/scaffold-model' +views = require 'kraken/base/scaffold/scaffold-view' +exports import models import views diff --git a/src/base/scaffold/scaffold-model.co b/src/base/scaffold/scaffold-model.co new file mode 100644 index 0000000..2800782 --- /dev/null +++ b/src/base/scaffold/scaffold-model.co @@ -0,0 +1,105 @@ +_ = require 'kraken/util/underscore' +op = require 'kraken/util/op' +{ BaseModel, BaseList, +} = require 'kraken/base' + + + +### Scaffold Models + +Field = exports.Field = BaseModel.extend do # {{{ + valueAttribute : 'value' + + defaults: -> + name : '' + type : 'String' + default : null + desc : '' + include : 'diff' + tags : [] + examples : [] + + + + constructor: function Field + BaseModel ... + + initialize: -> + _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse')) + @set 'id', @id = _.camelize @get 'name' + @set 'value', @get('default'), {+silent} if not @has 'value' + Field.__super__.initialize ... + + + + + + + /* * * Value Accessors * * */ + + getValue: (def) -> + @getParser() @get @valueAttribute, def + + setValue: (v, options) -> + def = @get 'default' + if not v and def == null + val = null + else + val = @getParser()(v) + @set @valueAttribute, val, options + + clearValue: -> + @set @valueAttribute, @get 'default' + + isDefault: -> + @get(@valueAttribute) is @get 'default' + + + /* * * Serializers * * */ + + serializeValue: -> + @serialize @getValue() + + toJSON: -> + {id:@id} import do + _.clone(@attributes) import { value:@getValue(), def:@get 'default' } + + toKVPairs: -> + { "#{@id}":@serializeValue() } + + toString: -> "(#{@id}: #{@serializeValue()})" + +# }}} + + +FieldList = exports.FieldList = BaseList.extend do # {{{ + model : Field + + constructor: function FieldList + BaseList ... + + + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */ + values: (opts={}) -> + opts = {+keepDefaults, -serialize} import opts + _.synthesize do + if opts.keepDefaults then @models else @models.filter -> not it.isDefault() + -> [ it.get('name'), if opts.serialize then it.serializeValue() else it.getValue() ] + + toJSON: -> + @values {+keepDefaults, -serialize} + + toKVPairs: -> + _.collapseObject @values {+keepDefaults, +serialize} + + toKV: (item_delim='&', kv_delim='=') -> + _.toKV @toKVPairs(), item_delim, kv_delim + + toURL: (item_delim='&', kv_delim='=') -> + "?#{@toKV ...}" + +# }}} + diff --git a/src/base/scaffold/scaffold-view.co b/src/base/scaffold/scaffold-view.co new file mode 100644 index 0000000..b5299bc --- /dev/null +++ b/src/base/scaffold/scaffold-view.co @@ -0,0 +1,125 @@ +_ = require 'kraken/util/underscore' +op = require 'kraken/util/op' +{ BaseView, +} = require 'kraken/base' +{ Field, FieldList, +} = require 'kraken/base/scaffold/scaffold-model' + + +FieldView = exports.FieldView = BaseView.extend do # {{{ + tagName : 'div' + className : 'field' + + type : 'string' + + events : + 'blur .value' : 'onChange' + 'submit .value' : 'onChange' + + + constructor: function FieldView + BaseView ... + + initialize: -> + # console.log "#this.initialize!" + BaseView::initialize ... + @type = @model.get 'type' .toLowerCase() or 'string' + + onChange: -> + if @type is 'boolean' + val = !! @$('.value').attr('checked') + else + val = @model.getParser() @$('.value').val() + + current = @model.getValue() + return if _.isEqual val, current + # console.log "#this.onChange( #current -> #val )" + @model.setValue val, {+silent} + @trigger 'change', this + + toTemplateLocals: -> + json = FieldView.__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)) + json + + /** + * A ghetto default template, typically overridden by superclass. + */ + template: (locals) -> + $ """ + + + """ + + render: -> + return @remove() if @model.get 'ignore' + FieldView.__super__.render ... + +# }}} + + +# There are several special options that, if passed, will be attached directly to the view: +# model, collection, el, id, className, tagName, attributes + +Scaffold = exports.Scaffold = BaseView.extend do # {{{ + __bind__ : <[ addField resetFields ]> + tagName : 'form' + className : 'scaffold' + + collectionType : FieldList + subviewType : FieldView + + + + constructor: function Scaffold + BaseView ... + + initialize: -> + CollectionType = @collectionType + @model = (@collection or= new CollectionType) + BaseView::initialize ... + + @collection.on 'add', @addField, this + @collection.on 'reset', @resetFields, this + + + + addField: (field) -> + @removeSubview field.view if field.view + + # avoid duplicating event propagation + field.off 'change:value', @onChange, this + + # propagate value-change events as key-value change events + field.on 'change:value', @onChange, this + + SubviewType = @subviewType + view = @addSubview new SubviewType model:field + view.on 'change', @onChange.bind(this, field) + + @render() + view + + resetFields: -> + @removeAllSubviews() + @collection.each @addField + this + + onChange: (field) -> + key = field.get 'name' + value = field.getValue() + @trigger "change:#key", this, value, key, field + @trigger "change", this, value, key, field + this + + + +# Proxy collection methods +<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]> + .forEach (methodname) -> + Scaffold::[methodname] = -> @collection[methodname].apply @collection, arguments + +# }}} + diff --git a/src/chart/chart-type.co b/src/chart/chart-type.co new file mode 100644 index 0000000..2840b9d --- /dev/null +++ b/src/chart/chart-type.co @@ -0,0 +1,425 @@ +moment = require 'moment' +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' +{ ReadyEmitter, +} = require 'kraken/util/event' +{ Parsers, ParserMixin, +} = require 'kraken/util/parser' + + + +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_TYPES = exports.KNOWN_CHART_TYPES = {} + + +/** + * @class Abstraction of a chart-type or charting library, encapsulating its + * logic and options. In addition, a `ChartType` also mediates the + * transformation of the domain-specific data types (the model and its view) + * with its specific needs. + * + * `ChartType`s mix in `ParserMixin`: when implementing a `ChartType`, you can + * add or supplement parsers merely by subclassing and overriding the + * corresponding `parseXXX` method (such as `parseArray` or `parseDate`). + * + * @extends EventEmitter + * @borrows ParserMixin + */ +class exports.ChartType extends ReadyEmitter + + ### Class Methods + + /** + * Register a new chart type. + */ + @register = (Subclass) -> + # console.log "ChartType.register(#Subclass)" + KNOWN_CHART_TYPES[ Subclass::typeName ] = Subclass + + /** + * Look up a `ChartType` by `typeName`. + */ + @lookup = (name) -> + name = name.get('chartType') if name instanceof Backbone.Model + KNOWN_CHART_TYPES[name] + + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {ChartType} + */ + @create = (model, view) -> + # console.log "ChartType.create(#model) ->", model + return null unless Type = @lookup model + new Type model, view + + + ### Class Properties + /* + * These are "class properties": each is set on the prototype at the class-level, + * and the reference is therefore shared by all instances. It is expected you + * will not modify this on the instance-level. + */ + + /** + * URL for the Chart Spec JSON. Loaded once, the first time an instance of + * that class is created. + * @type String + * @readonly + */ + SPEC_URL : null + + /** + * Chart-type name. + * @type String + * @readonly + */ + typeName: null + + /** + * Map of option name to ChartOption objects. + * @type { name:ChartOption, ... } + * @readonly + */ + options : null + + /** + * Ordered ChartOption objects. + * + * This is a "class-property": it is set on the prototype at the class-level, + * and the reference is shared by all instances. It is expected you will not + * modify that instance. + * + * @type ChartOption[] + * @readonly + */ + options_ordered : null + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + + /** + * Whether the ChartType has loaded all its data and is ready. + * @type Boolean + */ + ready: false + + + + ### Instance properties + + /** + * Model to be rendered as a chart. + * @type Backbone.Model + */ + model : null + + /** + * View to render the chart into. + * @type Backbone.View + */ + view : null + + /** + * Last chart rendered by this ChartType. + * @private + */ + chart: null + + + + + /** + * @constructor + */ + (@model, @view) -> + @roles or= {} + _.bindAll this, ...@__bind__ # TODO: roll up MRO + @loadSpec() unless @ready + + + # Builder Pattern + withModel : (@model) -> this + withView : (@view) -> this + + + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + loadSpec: -> + return this if @ready + proto = @constructor:: + jQuery.ajax do + url : @SPEC_URL + dataType : 'json' + success : (spec) ~> + proto.spec = spec + proto.options_ordered = spec + proto.options = _.synthesize spec, -> [it.name, it] + proto.ready = true + @triggerReady() + error: ~> console.error "Error loading #{@typeName} spec! #it" + this + + + /** + * @returns {ChartOption} Get an option's spec by name. + */ + getOption: (name, def) -> + @options[name] or def + + + /** + * @returns {Object} An object, mapping from option.name to the + * result of the supplied function. + */ + map: (fn, context=this) -> + _.synthesize @options, ~> [it.name, fn.call(context, it, it.name, this)] + + + /** + * @param {String} attr Attribute to look up on each options object. + * @returns {Object} Map from name to the value found at the given attr. + */ + pluck: (attr) -> + @map -> it[attr] + + + /** + * @returns {Boolean} Whether the supplied value is the same as + * the default value for the given key. + */ + isDefault: (name, value) -> + _.isEqual @getOption(name).default, value + + + ### }}} + ### Parsers & Serialization {{{ + + /** + * When implementing a ChartType, you can add or override parsers + * merely by subclassing. + * @borrows ParserMixin + */ + ParserMixin.mix this + + /** + * @returns {Function} Parser for the given option name. + */ + getParserFor: (name) -> + @getParser @getOption(name).type + + /** + * Parses a single serialized option value into its proper type. + * + * @param {String} name Option-name of the value being parsed. + * @param {String} value Value to parse. + * @returns {*} Parsed value. + */ + parseOption: (name, value) -> + @getParserFor(name)(value) + + /** + * Parses options using `parseOption(name, value)`. + * + * @param {Object} options Options to parse. + * @returns {Object} Parsed options. + */ + parseOptions: (options) -> + out = {} + for k, v in options + out[k] = @parseOption k, v + out + + + /** + * Serializes option-value to a String. + * + * @param {*} v Value to serialize. + * @param {String} k Option-name of the given value. + * @returns {String} The serialized value + */ + serialize: (v, k) -> + # if v!? + # v = '' + if _.isBoolean v + v = Number v + else if _.isObject v + v = JSON.stringify v + String v + + + ### }}} + ### Formatters {{{ + + /** + * 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. + * @param {Boolean} [abbrev=true] Expand number suffixes if false. + * @returns {Object} Formatted number parts. + */ + numberFormatter: (n, digits=2, abbrev=true) -> + suffixes = do + if abbrev + [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + else + [['Billion', 1000000000], ['Million', 1000000], ['', NaN]] + + for [suffix, d] of suffixes + 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, toString: -> + "#{@whole}#{@fraction}#{if abbrev then '' else ' '}#{@suffix}" + } + + + ### }}} + ### Rendering {{{ + + /** + * Finds the element in the view which plays the given role in the chart. + * Canonically, all charts have a "viewport" element. Other roles might + * include a "legend" element, or several "axis" elements. + * + * Default implementation looks up a selector in the `roles` hash, and if + * found, queries the view for matching children. + * + * @param {String} role Name of the role to look up. + * @returns {jQuery|null} $-wrapped DOM element. + */ + getElementsForRole: (role) -> + return null unless @view + if @roles[role] + @view.$ that + else + null + + + /** + * Transform/extract the data for this chart from the model. Default + * implementation calls `model.getData()`. + * + * @returns {*} Data object for the chart. + */ + getData: -> + @model.getData() + + + /** + * Map from option-name to default value. Note that this reference will be + * modified by `.render()`. + * + * @returns {Object} Default options. + */ + getDefaultOptions: -> + @pluck 'default' + + + + + /** + * Resizes the HTML viewport. Override to disable, etc. + */ + resizeViewport: -> + size = @determineSize() + @getElementsForRole 'viewport' .css size + size + + /** + * Determines chart viewport size. + * @return { width, height } + */ + determineSize: -> + modelW = width = @model.get 'width' + modelH = height = @model.get 'height' + return { width, height } unless @view.ready and width and height + + viewport = @getElementsForRole 'viewport' + + if width is 'auto' + Width = viewport.innerWidth() or 300 + width ?= modelW + + if height is 'auto' + height = viewport.innerHeight() or 320 + height ?= modelH + + { width, height } + + + /** + * Transforms domain data and applies it to the chart library to + * render or update the corresponding chart. + * + * @returns {Chart} + */ + render: -> + 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 + + + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * + * Default implementation returns `model.getOptions()`. + * + * @returns {Object} The derived data. + */ + transform: (model, view) -> + @model.getOptions() + + + /** + * Called to render the chart. + * + * @abstract + * @returns {Chart} + */ + renderChart: (data, viewport, options, lastChart) -> + ... + + + ### }}} + diff --git a/src/chart/index.co b/src/chart/index.co new file mode 100644 index 0000000..c5fd1e2 --- /dev/null +++ b/src/chart/index.co @@ -0,0 +1,8 @@ +chart_type = require 'kraken/chart/chart-type' +chart_option = require 'kraken/chart/option' +dygraphs = require 'kraken/chart/type/dygraphs' +d3_chart = require 'kraken/chart/type/d3-chart' +d3_elements = require 'kraken/chart/type/d3' + +exports import chart_type import chart_option \ + import dygraphs import d3_chart import d3_elements diff --git a/src/chart/option/chart-option-model.co b/src/chart/option/chart-option-model.co new file mode 100644 index 0000000..2a3c37a --- /dev/null +++ b/src/chart/option/chart-option-model.co @@ -0,0 +1,218 @@ +{ _, op, +} = require 'kraken/util' +{ Parsers, ParserMixin, ParsingModel, ParsingView, +} = require 'kraken/util/parser' +{ BaseModel, BaseList, +} = require 'kraken/base' + + +/** + * @class A set of tags. + */ +class exports.TagSet extends Array + tags : {} + + (values=[]) -> + @tags = {} + @add values if values?.length + + has: (tag) -> + @tags[tag]? + + get: (tag) -> + return -1 unless tag + unless @tags[tag]? + @tags[tag] = @length + @push tag + @tags[tag] + + update: (tags) -> + is_single = typeof tags is 'string' + tags = [tags] if is_single + indices = ( for tag of tags then @get tag ) + if is_single then indices[0] else indices + + toString: -> "TagSet(length=#{@length}, values=[\"#{@join '", "'}\"])" + + + +/** + * @namespace All known tags, for mapping consistently onto colors. + */ +KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet() + + + +/** + * @class Field with chart-option-specific handling for validation, parsing, tags, etc. + */ +ChartOption = exports.ChartOption = ParsingModel.extend do # {{{ + IGNORED_TAGS : <[ callback deprecated debugging ]> + valueAttribute : 'value' + + defaults: -> + name : '' + type : 'String' + default : null + desc : '' + include : 'diff' + tags : [] + examples : [] + + + + constructor: function ChartOption + ParsingModel ... + + initialize : -> + # console.log "#this.initialize!" + + # Bind all the `parseXXX()` methods so they can be passed about independent from the class + _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse')) + + ChartOption.__super__.initialize ... + @set 'id', @id = _.camelize @get 'name' + @set 'value', @get('default'), {+silent} if not @has 'value' + + + # Notify Tag indexer of category when created, to ensure all category-tags + # get indices with colors :P + KNOWN_TAGS.update @getCategory() + + # Ignore functions/callbacks and, ahem, hidden tags. + type = @get 'type' .toLowerCase() or '' + tags = @get('tags') or [] + if _.str.include(type, 'function') or _.intersection(tags, @IGNORED_TAGS).length + @set 'ignore', true + + + + ### Tag Handling + + # Wrapper to ensure @set('tags') is called, as tags.push() + # will not trigger the 'changed:tags' event. + addTag: (tag) -> + return this unless tag + tags = @get('tags') or [] + tags.push tag + @set 'tags', tags + this + + # Wrapper to ensure @set('tags') is called, as tags.push() + # will not trigger the 'changed:tags' event. + removeTag: (tag) -> + return this unless tag + tags = @get('tags') or [] + _.remove tags, tag + @set 'tags', tags + this + + # Keep tag list up to date + onTagUpdate: -> + KNOWN_TAGS.update @get 'tags' + this + + getTagIndex: (tag) -> + KNOWN_TAGS.get tag + + # A field's category is its first tag. + getCategory: -> + tags = (@get('tags') or [])[0] + + getCategoryIndex: -> + @getTagIndex @getCategory() + + + + /* * * Value Accessors * * */ + + getValue: (def) -> + @getParser() @get @valueAttribute, def + + setValue: (v, options) -> + def = @get 'default' + if not v and def == null + val = null + else + val = @getParser()(v) + @set @valueAttribute, val, options + + clearValue: -> + @set @valueAttribute, @get 'default' + + isDefault: -> + @get(@valueAttribute) is @get 'default' + + + + /* * * Serialization * * */ + + /** + * Override to default `type` to the model attribute of the same name. + * @returns {Function} Parser for the given type. + */ + getParser: (type) -> + type or= @get('type') or 'String' + ChartOption.__super__.getParser.call this, type + + serializeValue: -> + @serialize @getValue() + + toJSON: -> + {id:@id} import do + _.clone(@attributes) import { value:@getValue(), def:@get 'default' } + + toKVPairs: -> + { "#{@id}":@serializeValue() } + + toString: -> "(#{@id}: #{@serializeValue()})" + +# }}} + + + +/** + * @class List of ChartOption fields. + */ +ChartOptionList = exports.ChartOptionList = BaseList.extend do # {{{ + model : ChartOption + + + constructor: function ChartOptionList + BaseList ... + + + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepDefaults=true] If false, exclude pairs that + * haven't changed from their default value. + * @param {Boolean} [opts.serialize=false] If true, replace each value + * with its String version by calling `value.serializeValue()`. + * @returns {Object} Map of fields to their values. + */ + values: (opts={}) -> + opts = {+keepDefaults, -serialize} import opts + _.synthesize do + if opts.keepDefaults then @models else @models.filter -> not it.isDefault() + -> [ it.get('name'), if opts.serialize then it.serializeValue() else it.getValue() ] + + toJSON: -> + @values {+keepDefaults, -serialize} + + /** + * Override to omit defaults from URL. + */ + toKVPairs: -> + _.collapseObject @values {-keepDefaults, +serialize} + + toKV: (item_delim='&', kv_delim='=') -> + _.toKV @toKVPairs(), item_delim, kv_delim + + toURL: (item_delim='&', kv_delim='=') -> + "?#{@toKV ...}" + + +# }}} + diff --git a/src/chart/option/chart-option-view.co b/src/chart/option/chart-option-view.co new file mode 100644 index 0000000..1b004e1 --- /dev/null +++ b/src/chart/option/chart-option-view.co @@ -0,0 +1,270 @@ +{ _, op, +} = require 'kraken/util' +{ BaseView, +} = require 'kraken/base' +{ ChartOption, ChartOptionList, +} = require 'kraken/chart/option/chart-option-model' + +DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms + + +/** + * @class View for a single configurable option in a chart type. + */ +ChartOptionView = exports.ChartOptionView = BaseView.extend do # {{{ + tagName : 'section' + className : 'chart-option field' + template : require 'kraken/template/chart/chart-option' + + type : 'string' + isCollapsed : true + + events : + 'blur .value' : 'onChange' + 'click input[type="checkbox"].value' : 'onChange' + 'submit .value' : 'onChange' + 'click .close' : 'toggleCollapsed' + 'click h3' : 'toggleCollapsed' + 'click .collapsed' : 'onClick' + + + + constructor: function ChartOptionView + BaseView ... + + initialize: -> + ChartOptionView.__super__.initialize ... + @type = @model.get 'type' .toLowerCase() or 'string' + + + /* * * * Rendering * * * */ + + toTemplateLocals: -> + json = ChartOptionView.__super__.toTemplateLocals ... + json.id or= _.camelize json.name + json.value ?= '' + v = json.value + json.value = JSON.stringify(v) if v and ( _.isArray(v) or _.isPlainObject(v) ) + json + + /** + * Override to annotate with collapsed state and to kill off ignored options + * so they do not contribute their values when looking at form updates. + */ + render: -> + return @remove() if @model.get 'ignore' + ChartOptionView.__super__.render ... + @$el.addClass 'collapsed' if @isCollapsed + this + + + + /* * * * Option Collapsing * * * */ + + /** + * Sets the state of `isCollapsed` and updates the UI. If the state changed, + * a `'change:collapse`` event will be fired.` + * + * @param {Boolean} [makeCollapsed=true] If true, set state to collapsed. + * @returns {Boolean} Whether the state changed. + */ + collapse: (state=true) -> + state = !! state + @isCollapsed = @$el.hasClass 'collapsed' + + return this if state is @isCollapsed + if state + @$el.addClass 'collapsed' + else + @$el.removeClass 'collapsed' + @isCollapsed = state + @trigger 'change:collapse', this, @isCollapsed + true + + /** + * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event. + * @returns {this} + */ + toggleCollapsed: -> + @collapse not @$el.hasClass 'collapsed' + this + + + + /* * * * Events * * * */ + + /** + * To prevent `toggleCollapsed()` from being called multiple times due to + * overlapping listeners, we're only looking for clicks on the collapsed header. + */ + onClick: (evt) -> + target = $ evt.target + @toggleCollapsed() if @$el.hasClass('collapsed') and not target.hasClass('close') + + /** + * Propagate user input changes to the model, and upward to the parent view. + */ + onChange: -> + if @type is 'boolean' + val = !! @$('.value').attr('checked') + else + val = @model.getParser() @$('.value').val() + + current = @model.getValue() + return if _.isEqual val, current + console.log "#this.onChange( #current -> #val )" + @model.setValue val, {+silent} + @trigger 'change', @model, this + # false + + +# }}} + + + +/** + * @class View for configuring a chart type. + */ +ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend do # {{{ + __bind__ : <[ addField ]> + tagName : 'form' + className : 'chart-options scaffold' + template : require 'kraken/template/chart/chart-scaffold' + + collectionType : ChartOptionList + subviewType : ChartOptionView + + events: + 'click .options-filter-button' : 'onFilterOptions' + 'click .collapse-all-options-button' : 'collapseAll' + 'click .expand-all-options-button' : 'expandAll' + + + + + constructor: function ChartOptionScaffold + BaseView ... + + initialize : -> + @render = _.debounce @render.bind(this), DEBOUNCE_RENDER + CollectionType = @collectionType + @model = (@collection or= new CollectionType) + ChartOptionScaffold.__super__.initialize ... + + @collection.on 'add', @addField, this + @collection.on 'reset', @onReset, this + @on 'render', @onRender, this + + + /** + * Bookkeeping for new ChartOptions, creating it a new subview and subscribing + * to its activity, and then rendering it. + * @returns {ChartOptionView} The Option's new view. + */ + addField: (field) -> + @removeSubview field.view if field.view + + # avoid duplicating event propagation + field.off 'change:value', @onChange, this + + # propagate value-change events as key-value change events + field.on 'change:value', @onChange, this + + SubviewType = @subviewType + @addSubview view = new SubviewType model:field + .on 'change', @onChange.bind(this, field) + .on 'change:collapse', @render, this + + @render() # WTF: hmm. + view + + + /* * * * UI * * * */ + + /** + * Collapse all expanded subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */ + collapseAll: -> + _.invoke @subviews, 'collapse', true + false + + /** + * Expand all collapsed subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */ + expandAll: -> + _.invoke @subviews, 'collapse', false + false + + /** + * Reflow Isotope post-`render()`. + */ + onRender: -> + # console.log "#this.onRender(ready=#{@ready}) -> .isotope()" + + # The DOM doesn't calculate dimensions of elements that are not visible, + # which makes it impossible for Isotope to do its job. + return unless @$el.is ':visible' + + # Invoke Isotope to re-layout the option elements + @$ '.isotope' .isotope do + # itemPositionDataEnabled : true + itemSelector : '.chart-option.field' + layoutMode : 'masonry' + masonry : { columnWidth:10 } + filter : @getOptionsFilter() + sortBy : 'category' + getSortData : + category: ($el) -> + $el.data 'model' .getCategory() + + /** + * @returns {String} Selector representing the selected set of Option filters. + */ + getOptionsFilter: -> + data = @$ '.options-filter-button.active' .toArray().map -> $ it .data() + sel = data.reduce do + (sel, d) -> sel += if d.filter then that else '' + ':not(.ignore)' + sel + + + + /* * * * Events * * * */ + + /** + * Propagate change events from fields as if they were attribute changes. + * Note: `field` is bound to the handler + */ + onChange: (field) -> + key = field.get 'name' + value = field.getValue() + @trigger "change:#key", this, value, key, field + @trigger "change", this, value, key, field + this + + onReset: -> + # The collection has been reset, assume all subviews are + # invalid and rebuild them. + @removeAllSubviews() + @collection.each @addField + _.defer @render + + onFilterOptions: (evt) -> + evt.preventDefault() + # Defer re-rendering until after we yield for the DOM to do its thang + _.defer @render + + +# Proxy collection methods +<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]> + .forEach (methodname) -> + ChartOptionScaffold::[methodname] = -> @collection[methodname].apply @collection, arguments + + +# }}} + + diff --git a/src/chart/option/index.co b/src/chart/option/index.co new file mode 100644 index 0000000..5cdea4d --- /dev/null +++ b/src/chart/option/index.co @@ -0,0 +1,4 @@ +model = require 'kraken/chart/option/chart-option-model' +view = require 'kraken/chart/option/chart-option-view' + +exports import model import view diff --git a/src/chart/type/d3-chart.co b/src/chart/type/d3-chart.co new file mode 100644 index 0000000..02c2c34 --- /dev/null +++ b/src/chart/type/d3-chart.co @@ -0,0 +1,138 @@ +d3 = require 'd3' +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ ChartType, +} = require 'kraken/chart/chart-type' +{ D3ChartElement, +} = require 'kraken/chart/type/d3/d3-chart-element' + + +root = do -> this + + +class exports.D3ChartType extends ChartType + __bind__ : <[ determineSize ]> + SPEC_URL : '/schema/d3/d3-chart.json' + + # NOTE: ChartType.register() must come AFTER `typeName` declaration. + typeName : 'd3-chart' + ChartType.register this + + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + legend : '.graph-legend' + + + + -> super ... + + + getData: -> + @model.dataset.getColumns() + + + transform: -> + dataset = @model.dataset + options = @model.getOptions() import @determineSize() + options import do + colors : dataset.getColors() + labels : dataset.getLabels() + options + + + renderChart: (data, viewport, options, lastChart) -> + ### Starting with http://bost.ocks.org/mike/chart/ + + + # margin convention http://bl.ocks.org/3019563 + margin = {top: 20, right: 20, bottom: 20, left: 20} + width = 760 - margin.left - margin.right + height = 320 - margin.top - margin.bottom + + xScale = d3.time.scale() + yScale = d3.scale.linear() + + dates = data[0] + cols = data.slice(1) + + # Calculate extents using all the data points (but not dates) + # allValues = d3.merge @model.dataset.getDataColumns() + allValues = d3.merge cols + + # Update the x-scale with the extents of the dates. + xScale + .domain d3.extent dates + .range [ 0, width ] + + # Update the y-scale with the extents of the data. + yScale + .domain d3.extent allValues + .range [ height, 0 ] + + + # Hack. Remove svg if it exists. If @model changes, the graph will be redrawn + svg = d3.select viewport.0 .selectAll "svg" + .remove() + # Select the svg element, if it exists. + svg = d3.select viewport.0 .selectAll "svg" + .data [cols] + # ...Otherwise, create the skeletal chart. + enterFrame = svg.enter() + .append "svg" .append "g" + .attr "class", "frame" + + # Update chart dimensions. + svg .attr "width", width + margin.left + margin.right + .attr "height", height + margin.top + margin.bottom + + frame = svg.select "g.frame" + .attr "transform", "translate(#{margin.left},#{margin.top})" + .attr "width", width + .attr "height", height + + + + # x-axis. + # TODO move axis to separate chart-type + enterFrame.append "g" + .attr "class", "x axis time" + + + xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0) + frame.select ".x.axis.time" + .attr "transform", "translate(0,#{yScale.range()[0]})" + .call xAxis + + + metrics = frame.selectAll "metric" + .data @model.dataset.metrics.models + + metrics.enter() + .append("g") + .attr "class", (d) -> + "g metric line "+d.get 'label' + .each (d) -> + # console.log d + # metric defined charttype + chartElement = d.get "chartElement" + # otherwise the graph defined charttype + # FOR NOW take line as default + chartElement ?= 'd3-line' # @model.get "chartElement" + # create d3 chart element and render it + chEl = D3ChartElement.create chartElement + + chEl.renderChartElement d, frame ,xScale, yScale + + metrics.exit().remove() + + svg + + diff --git a/src/chart/type/d3/d3-bar-chart-type.co b/src/chart/type/d3/d3-bar-chart-type.co new file mode 100644 index 0000000..6bab42b --- /dev/null +++ b/src/chart/type/d3/d3-bar-chart-type.co @@ -0,0 +1,239 @@ +d3 = require 'd3' + +{ _, op, +} = require 'kraken/util' +{ ChartType, +} = require 'kraken/chart/chart-type' + +root = do -> this + + +class exports.BarChartType extends ChartType + __bind__ : <[ determineSize ]> + SPEC_URL : '/schema/d3/d3-bar.json' + + # NOTE: ChartType.register() must come AFTER `typeName` declaration. + typeName : 'd3-bar' + ChartType.register this + + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + legend : '.graph-legend' + + + -> super ... + + getData: -> + @model.dataset.getColumns() + + + transform: -> + dataset = @model.dataset + options = @model.getOptions() import @determineSize() + options import do + colors : dataset.getColors() + labels : dataset.getLabels() + options + + + renderChartType: (metric, svgEl ,xScale, yScale) -> + + X = (d, i) -> xScale d[0] + Y = (d, i) -> yScale d[1] + + + ### Render the line path + metricBars = root.metricBars = svgEl.append "g" + .attr "class", "metric bars "+metric.get 'label' + + data = d3.zip metric.getDateColumn(),metric.getData() + + ### Render Bars + barWidth = svgEl.attr('width')/data.length + barHeight = (d) -> svgEl.attr('height')-Y(d) + + metricBars.selectAll "bar" + .data data + .enter().append "rect" + .attr "class", (d, i) -> "metric bar #i" + .attr "x", X + .attr "y", Y + .attr "height", barHeight + .attr "width", -> barWidth + .attr "fill", metric.get 'color' + .attr "stroke", "white" + .style "opacity", "0.4" + .style "z-index", -10 + + + # adding event listeners + chT = this + metricBars.selectAll ".metric.bar" + .on "mouseover", (d, i) -> + + svgEl.append "text" + .attr "class", "mf" + .attr "dx", 50 + .attr "dy", 100 + .style "font-size", "0px" + .transition() + .duration(800) + .text "Uh boy, the target would be: "+chT.numberFormatter(d[1]).toString() + .style "font-size", "25px" + .on "mouseout", (d, i) -> + + svgEl.selectAll ".mf" + .transition() + .duration(300) + .text "BUMMER!!!" + .style "font-size", "0px" + .remove() + + + + svgEl + + renderChart: (data, viewport, options, lastChart) -> + ### Starting with http://bost.ocks.org/mike/chart/ + + margin = {top: 20, right: 20, bottom: 20, left: 20} + width = 760 + height = 320 + xScale = d3.time.scale() + yScale = d3.scale.linear() + + dates = data[0] + cols = data.slice(1) + + # Calculate extents using all the data points (but not dates) + # allValues = d3.merge @model.dataset.getDataColumns() + allValues = d3.merge cols + + + # Update the x-scale with the extents of the dates. + xScale + .domain d3.extent dates + .range [ 0, width - margin.left - margin.right ] + + # Update the y-scale with the extents of the data. + yScale + .domain d3.extent allValues + .range [ height - margin.top - margin.bottom, 0 ] + + # Select the svg element, if it exists. + svg = d3.select viewport.0 .selectAll "svg" + .data [cols] + + # ...Otherwise, create the skeletal chart. + enterFrame = svg.enter() + .append "svg" .append "g" + .attr "class", "frame" + enterFrame.append "g" + .attr "class", "x axis time" + + # Update chart dimensions. + svg .attr "width", width + .attr "height", height + frame = svg.select "g.frame" + .attr "transform", "translate(#{margin.left},#{margin.top})" + + # Update the x-axis. + xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0) + frame.select ".x.axis.time" + .attr "transform", "translate(0,#{yScale.range()[0]})" + .call xAxis + + X = (d, i) -> xScale d[0] + Y = (d, i) -> yScale d[1] + + ### Render Bars + barWidth = svg.attr('width')/dates.length + barHeight = (d) -> svg.attr('height')-Y(d) + + bars = frame.selectAll "g.bars" + .data cols.map -> d3.zip dates, it + bars.enter().append "g" + .attr "class", (col, i) -> "metric bars #i" + bars.exit().remove() + + bars.selectAll ".bar" + .data op.first + .enter().append "rect" + .attr "class", "bar" + .attr "x", X + .attr "y", Y + .attr "height", barHeight + .attr "width", -> barWidth + # TODO grab color from graph spec + .attr "fill", "red" + .attr "stroke", "white" + + + ### Mouse Lens + lens = root.lens = frame.selectAll "g.lens" + .data [[]] + gLens = lens.enter().append "g" + .attr "class", "lens" + .style "z-index", 1e9 + gInner = gLens.append "g" + .attr "transform", "translate(1.5em,0)" + gInner.append "circle" + .attr "r", "1.5em" + # .style "opacity", "0.4" + # .style "fill", "white" + .style "fill", "rgba(255, 255, 255, 0.4)" + .style "stroke", "white" + .style "stroke-width", "3px" + gInner.append "text" + .attr "y", "0.5em" + .attr "text-anchor", "middle" + .style "fill", "white" + .style "font", "12px Helvetica" + .style "font-weight", "bold" + + + mf = frame.selectAll "g.mf" + .data ["mf"] + .enter().append "g" + .attr "class", "mf" + .append "text" + .attr "class", "yoyo" + .attr "dx", 50 + .attr "dy", 100 + + + + bars.selectAll ".bar" + .on "mouseover", (d, i) -> + el = root.el = el # DOM element of event + # {r,g,b} = color = d3.rgb options.colors[i] + mf + .transition() + .duration(300) + .ease("exp") + .text "Uh boy, the target would be:"+d[1] + .style "font-size", "25px" + + .on "mouseout", (d, i) -> + mf + .transition() + .duration(1000) + .text "BUMMER!!!" + .style "font-size", "0px" + + + # {x:lineX, y:lineY} = root.pt = line.indexToPoint idx + # lens = frame.select "g.lens" + # .attr "transform", "translate(#lineX, #lineY)" + # lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" + # lens.select "text" .text Y + + + + svg diff --git a/src/chart/type/d3/d3-bar-element.co b/src/chart/type/d3/d3-bar-element.co new file mode 100644 index 0000000..9f93fa7 --- /dev/null +++ b/src/chart/type/d3/d3-bar-element.co @@ -0,0 +1,78 @@ +d3 = require 'd3' + +{ _, op, +} = require 'kraken/util' +{ D3ChartElement +} = require 'kraken/chart/type/d3/d3-chart-element' + +_fmt = require 'kraken/util/formatters' + +root = do -> this + +class exports.BarChartType extends D3ChartElement + __bind__ : <[ ]> + SPEC_URL : '/schema/d3/d3-bar.json' + + # NOTE: D3ChartElement.register() must come AFTER `typeName` declaration. + chartElement : 'd3-bar' + D3ChartElement.register this + + -> super ... + + renderChartElement: (metric, svgEl ,xScale, yScale) -> + + X = (d, i) -> xScale d[0] + Y = (d, i) -> yScale d[1] + + + ### Render the line path + metricBars = root.metricBars = svgEl.append "g" + .attr "class", "metric bars "+metric.get 'label' + + data = d3.zip metric.getDateColumn(),metric.getData() + + ### Render Bars + barWidth = svgEl.attr('width')/data.length + barHeight = (d) -> svgEl.attr('height')-Y(d) + + metricBars.selectAll "bar" + .data data + .enter().append "rect" + .attr "class", (d, i) -> "metric bar #i" + .attr "x", X + .attr "y", Y + .attr "height", barHeight + .attr "width", barWidth + .attr "fill", metric.get 'color' + .attr "stroke", "white" + .style "opacity", "0.4" + .style "z-index", -10 + + + # adding event listeners + chT = this + metricBars.selectAll ".metric.bar" + .on "mouseover", (d, i) -> + + svgEl.append "text" + .attr "class", "mf" + .attr "dx", 50 + .attr "dy", 100 + .style "font-size", "0px" + .transition() + .duration(800) + .text "Uh boy, the target would be: "+_fmt.numberFormatter(d[1]).toString() + .style "font-size", "25px" + .on "mouseout", (d, i) -> + + svgEl.selectAll ".mf" + .transition() + .duration(300) + .text "BUMMER!!!" + .style "font-size", "0px" + .remove() + + + + svgEl + diff --git a/src/chart/type/d3/d3-chart-element.co b/src/chart/type/d3/d3-chart-element.co new file mode 100644 index 0000000..29cc2a3 --- /dev/null +++ b/src/chart/type/d3/d3-chart-element.co @@ -0,0 +1,84 @@ +d3 = require 'd3' +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ ReadyEmitter, +} = require 'kraken/util/event' +# Base = require 'kraken/base/base' + + +root = do -> this + +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_ELEMENTS = exports.KNOWN_CHART_ELEMENTS = {} + +class exports.D3ChartElement extends ReadyEmitter + __bind__ : <[ ]> + SPEC_URL : '/schema/d3/d3-chart.json' + + + ### Class Methods + + /** + * Register a new d3 element + */ + @register = (Subclass) -> + # console.log "D3ChartElement.register(#Subclass)" + KNOWN_CHART_ELEMENTS[ Subclass::chartElement ] = Subclass + + /** + * Look up a `charttype` by `typeName`. + */ + @lookup = (name) -> + name = name.get('chartElement') if name instanceof Backbone.Model + KNOWN_CHART_ELEMENTS[name] + + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {D3ChartElement} + */ + @create = (name) -> + # console.log "D3ChartElement.create(#name)" + return null unless Type = @lookup name + + new Type + + + -> + _.bindAll this, ...@__bind__ # TODO: roll up MRO + @loadSpec() unless @ready + super ... + + + + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + loadSpec: -> + return this if @ready + proto = @constructor:: + jQuery.ajax do + url : @SPEC_URL + dataType : 'json' + success : (spec) ~> + proto.spec = spec + proto.options_ordered = spec + proto.options = _.synthesize spec, -> [it.name, it] + proto.ready = true + @triggerReady() + error: ~> console.error "Error loading #{@typeName} spec! #it" + + this + + renderChartElement: (metric, svgEl ,xScale, yScale) -> svgEl + + + + + diff --git a/src/chart/type/d3/d3-geo-element.co b/src/chart/type/d3/d3-geo-element.co new file mode 100644 index 0000000..2238b31 --- /dev/null +++ b/src/chart/type/d3/d3-geo-element.co @@ -0,0 +1,185 @@ +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ ChartType, +} = require 'kraken/chart/chart-type' + + + + +class exports.GeoWorldChartType extends ChartType + __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> + SPEC_URL : '/schema/d3/d3-geo-world.json' + + # NOTE: ChartType.register() must come AFTER `typeName` declaration. + typeName : 'd3-geo-world' + ChartType.register this + + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + legend : '.graph-legend' + + + + -> super ... + + + transform: -> + options = @model.getOptions() import @determineSize() + # options.colors.palette = ["black", "red"] if options.colors.palette? + options.colors.scaleDomain = d3.extent if options.colors.scaleDomain? + options + + + getProjection : (type) -> + switch type + case 'mercator' 'albers' 'albersUsa' + d3.geo[type]() + case 'azimuthalOrtho' + d3.geo.azimuthal() + .mode 'orthographic' + case 'azimuthalStereo' + d3.geo.azimuthal() + .mode 'stereographic' + default + throw new Error "Invalid map projection type '#type'!" + + + renderChart: (data, viewport, options, lastChart) -> + {width, height} = options + + fill = @fill = (data, options) -> + d3.scale[ options.colors.scale ]() + .domain options.colors.scaleDomain + .range options.colors.palette + + quantize = @quantize = (data, options) -> + (d) -> + if data[d.properties.name]? + return fill data[d.properties.name].editors + else + # console.log 'Country '+d.properties.name+' not in data' + return fill "rgb(0,0,0)" + + projection = @projection = @getProjection(options.map.projection) + .scale width + .translate [width/2, height/2] + + path = d3.geo.path() + .projection projection + + # path objects + feature = map.selectAll ".feature" + infobox = d3.select '.infobox' + + + move = -> + projection + .translate d3.event.translate + .scale d3.event.scale + feature.attr "d", path + + zoom = d3.behavior.zoom() + .translate projection.translate() + .scale projection.scale() + .scaleExtent [height,height*8] + .on "zoom", move + + + #### + + chart = d3.select viewport.0 + .append "svg:svg" + .attr "width", width + .attr "height", height + .append "svg:g" + .attr "transform", "translate(0,0)" + .call zoom + + # rectangle + map.append "svg:rect" + .attr "class", "frame" + .attr "width", width + .attr "height", height + + + ### infobox + infobox.select '#ball' + .append "svg:svg" + .attr "width", "100%" + .attr "height", "20px" + .append "svg:rect" + .attr "width", "60%" + .attr "height", "20px" + .attr "fill", '#f40500' + + setInfoBox = (d) -> + name = d.properties.name + ae = 0 + e5 = 0 + e100 = 0 + + if data[name]? + ae = parseInt data[name].editors + e5 = parseInt data[name].editors5 + e100 = parseInt data[name].editors100 + + infobox.select '#country' .text name + infobox.select '#ae' .text ae + infobox.select '#e5' .text e5+" ("+(100.0*e5/ae).toPrecision(3)+"%)" + infobox.select '#e100' .text e100+" ("+(100.0*e100/ae).toPrecision(3)+"%)" + + xy = d3.svg.mouse this + infobox.style "left", xy[0]+'px' + infobox.style "top", xy[1]+'px' + infobox.style "display", "block" + + + worldmap = -> + d3.json do + "/data/geo/maps/world-countries.json" + (json) -> + feature := feature + .data json.features + .enter().append "svg:path" + .attr "class", "feature" + .attr "d", path + .attr "fill", quantize + .attr "id", (d) -> d.properties.name + .on "mouseover", setInfoBox + .on "mouseout", -> infobox.style "display", "none" + + + + + +data = null +main = -> + jQuery.ajax do + url : "/data/geo/data/en_geo_editors.json" + dataType : 'json' + success : (res) -> + # result will be the returned JSON + data := res + + # delete & hide spinner + jQuery '.geo-spinner' .spin(false).hide() + + # load the world map + worldmap() + + # adding bootstrap tooltips + # $ '.page-header' .tooltip title:"for the header it works but is useless" + # $ '.feature' .tooltip title:"here it doesn't work" + + console.log 'Loaded geo coding map!' + error : (err) -> console.error err + + diff --git a/src/chart/type/d3/d3-line-element.co b/src/chart/type/d3/d3-line-element.co new file mode 100644 index 0000000..51ce742 --- /dev/null +++ b/src/chart/type/d3/d3-line-element.co @@ -0,0 +1,111 @@ +d3 = require 'd3' +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ D3ChartElement +} = require 'kraken/chart/type/d3/d3-chart-element' + +_fmt = require 'kraken/util/formatters' + +root = do -> this + +class exports.LineChartElement extends D3ChartElement + __bind__ : <[ ]> + SPEC_URL : '/schema/d3/d3-line.json' + + # NOTE: D3ChartElement.register() must come AFTER `typeName` declaration. + chartElement : 'd3-line' + D3ChartElement.register this + + -> super ... + + renderChartElement: (metric, svgEl ,xScale, yScale) -> + + X = (d, i) -> xScale d[0] + Y = (d, i) -> yScale d[1] + line = d3.svg.line().x(X).y(Y) + + ### Render the line path + metricLine = root.metricLine = svgEl.append "g" + .attr "class", "g metric line "+metric.get 'label' + + data = d3.zip metric.getDateColumn(),metric.getData() + + metricLine.selectAll "path.line" + .data d3.zip data.slice(0,-1), data.slice(1) + .enter().append "path" + .attr "d", line + .attr "class", (d, i) -> "metric line segment #i" + .style "stroke", metric.getColor 'color' + + + ### Mouse Lens + lens = root.lens = svgEl.selectAll "g.lens" + .data [[]] + gLens = lens.enter().append "g" + .attr "class", "lens" + .style "z-index", 1e9 + gInner = gLens.append "g" + .attr "transform", "translate(1.5em,0)" + gInner.append "circle" + .attr "r", "1.5em" + # .style "opacity", "0.4" + # .style "fill", "white" + .style "fill", "rgba(255, 255, 255, 0.4)" + .style "stroke", "white" + .style "stroke-width", "3px" + gInner.append "text" + .attr "y", "0.5em" + .attr "text-anchor", "middle" + .style "fill", "black" + .style "font", "12px Helvetica" + .style "font-weight", "bold" + + # event listeners + metricLine.selectAll ".line.segment" + .on "mouseover", (d, i) -> + + {r,g,b} = color = d3.rgb metric.getColor 'color' + lineX = (X(d[0])+X(d[1]))/2 + lineY = (Y(d[0])+Y(d[1]))/2 + + + lens = svgEl.select "g.lens" + .attr "transform", "translate(#lineX, #lineY)" + lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" + lens.select "text" .text -> _fmt.numberFormatter(d[0][1]).toString() + + svgEl + + +# If instead of a line segment for each month we want to use one line segment per metric, we have to have a way of accessing the data associated with a given time step. + +# lines.attr "d", line +# .attr "class", (col, i) -> "metric line metric#i" +# .style "stroke", (col, i) -> options.colors[i] +# .each (col, i) -> +# {width} = bbox = @getBBox() +# # Add line-to-data position conversions +# @indexAtX = d3.scale.quantize() +# .domain [0, width] +# .range d3.range col.length +# @indexToPoint = (idx) -> +# @pathSegList.getItem idx + +# lines.on "mouseover", (col, i) -> +# line = root.line = this # DOM element of event +# {r,g,b} = color = d3.rgb options.colors[i] + +# # quantize mouse x-location to get for closest data-point (index into data array) +# [x,y] = root.pos = d3.mouse line +# idx = root.idx = line.indexAtX x +# {x:lineX, y:lineY} = root.pt = line.indexToPoint idx + +# lens = frame.select "g.lens" +# .data d3.select(line).data() +# .attr "transform", "translate(#lineX, #lineY)" +# lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" +# lens.select "text" .text (col) -> col[idx][1] + + diff --git a/src/chart/type/d3/index.co b/src/chart/type/d3/index.co new file mode 100644 index 0000000..a18badd --- /dev/null +++ b/src/chart/type/d3/index.co @@ -0,0 +1,6 @@ +d3chart = require 'kraken/chart/type/d3/d3-chart-element' +line = require 'kraken/chart/type/d3/d3-line-element' +bar = require 'kraken/chart/type/d3/d3-bar-element' +# geo = require 'kraken/chart/type/d3/d3-geo-element' + +exports import line import bar import d3chart # import geo diff --git a/src/chart/type/dygraphs.co b/src/chart/type/dygraphs.co new file mode 100644 index 0000000..4d0113e --- /dev/null +++ b/src/chart/type/dygraphs.co @@ -0,0 +1,133 @@ +_ = require 'kraken/util/underscore' +{ ChartType, +} = require 'kraken/chart/chart-type' + + +class exports.DygraphsChartType extends ChartType + __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> + SPEC_URL : '/schema/dygraph.json' + + # NOTE: ChartType.register() must come AFTER `typeName` declaration. + typeName : 'dygraphs' + ChartType.register this + + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + legend : '.graph-legend' + + -> super ... + + + + ### Formatters {{{ + + # XXX: Dygraphs-specific + makeAxisFormatter: (fmttr) -> + (n, granularity, opts, g) -> fmttr n, opts, g + + # XXX: Dygraphs-specific + dygAxisDateFormatter: (n, granularity, opts, g) -> + moment(n).format 'MM/YYYY' + + # XXX: Dygraphs-specific + dygDateFormatter: (n, opts, g) -> + moment(n).format 'DD MMM YYYY' + + # XXX: Dygraphs-specific + dygNumberFormatter: (n, opts, g) -> + digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2 + { whole, fraction, suffix } = @numberFormatter n, digits + "#whole#fraction#suffix" + + # XXX: Dygraphs-specific + dygNumberFormatterHTML: (n, opts, g) -> + digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2 + # digits = opts('digitsAfterDecimal') ? 2 + { whole, fraction, suffix } = @numberFormatter n, digits + # coco will trim the whitespace + " + #whole + #fraction + #suffix + " + + + ### }}} + ### Rendering {{{ + + /** + * Determines chart viewport size. + * @return { width, height } + */ + determineSize: -> + modelW = width = @model.get 'width' + modelH = height = @model.get 'height' + return { width, height } unless @view.ready and width and height + + viewport = @getElementsForRole 'viewport' + legend = @getElementsForRole 'legend' + + if width is 'auto' + # Remove old style, as it confuses dygraph after options update + delete viewport.prop('style').width + vpWidth = viewport.innerWidth() or 300 + legendW = legend.outerWidth() or 228 + width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW) + width ?= modelW + + if height is 'auto' + # Remove old style, as it confuses dygraph after options update + delete viewport.prop('style').height + height = viewport.innerHeight() or 320 + height ?= modelH + + { width, height } + + + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * @returns {Object} The derived chart options. + */ + transform: -> + dataset = @model.dataset + options = @view.chartOptions() import @determineSize() + options import do + colors : dataset.getColors() + labels : dataset.getLabels() + labelsDiv : @getElementsForRole 'legend' .0 + valueFormatter : @dygNumberFormatterHTML + axes: + x: + axisLabelFormatter : @dygAxisDateFormatter + valueFormatter : @dygDateFormatter + y: + axisLabelFormatter : @makeAxisFormatter @dygNumberFormatter + valueFormatter : @dygNumberFormatterHTML + + + /** + * @returns {Dygraph} The Dygraph chart object. + */ + renderChart: (data, viewport, options, lastChart) -> + @resizeViewport() + + # console.log "#this.render!" + # _.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. + lastChart?.destroy() + new Dygraph viewport.0, data, options + + + + ### }}} + + diff --git a/src/chart/type/index.co b/src/chart/type/index.co new file mode 100644 index 0000000..e69de29 diff --git a/src/dashboard/dashboard-model.co b/src/dashboard/dashboard-model.co new file mode 100644 index 0000000..d4e3fbf --- /dev/null +++ b/src/dashboard/dashboard-model.co @@ -0,0 +1,87 @@ +{ _, op, +} = require 'kraken/util' +{ BaseModel, +} = require 'kraken/base' +{ Graph, GraphList, +} = require 'kraken/graph/graph-model' + + +/** + * @class + */ +Dashboard = exports.Dashboard = BaseModel.extend do # {{{ + urlRoot : '/dashboards' + + # graph_ids : null + graphs : null + # tabs : null + + + constructor: function Dashboard + @graphs = new GraphList + BaseModel ... + + + initialize: -> + BaseModel::initialize ... + # @getGraphs() + + defaults: -> + name : null + tabs : [ { name:"Main", graph_ids:[] } ] + + load: -> + @once 'fetch-success', (~> @getGraphs()) .loadModel() + this + + + /** + * Look up a tab. + * + * @param {String|Number} tab Tab name or index. + * @returns {Tab} Tab object. + */ + getTab : (tab) -> + tabs = @get 'tabs' + return tabs[tab] if typeof tab is 'number' + _.find tabs, -> it.name is tab + +# addGraph: (graph, tabName) -> +# ... + + show : (cb, obj) -> + console.log('[show]') + console.log(obj) + cb null, obj + + pushAsync : (cb, arr) -> + (err, elem) -> + arr.push elem + cb null + + getGraphs : -> + console.log('[getGraphs]\tentering') + # consolidate graph_ids to one array + graph_ids = _(@tabs).chain().values().map((tab_obj) -> tab_obj.graph_ids).flatten().value() + + Seq graph_ids + .parMap_ (next, graph_id) -> + next null, [graph_id] + .parEach_ (next, graph_id_arr) ~> + Graph.lookup graph_id_arr[0], @pushAsync next, graph_id_arr + # Graph.lookup graph_id_arr[0], (err, el) -> + # graph_id_arr.push el + # next null + # .parEach_ @show + .parMap_ (next, [id, graph]:tuple) ~> + graph.once 'ready', -> next.ok tuple + .unflatten() + .seq_ (next, graph_tuples) ~> + # @graphs = _.generate graph_tuples + @graphs.reset _.pluck graph_tuples, 1 + console.log('[setter]\tcalling ready') + @triggerReady() + this + + +# }}} \ No newline at end of file diff --git a/src/dashboard/dashboard-view.co b/src/dashboard/dashboard-view.co new file mode 100644 index 0000000..eada37e --- /dev/null +++ b/src/dashboard/dashboard-view.co @@ -0,0 +1,174 @@ +Seq = require 'seq' + +{ _, op, +} = require 'kraken/util' +{ BaseModel, BaseView, +} = require 'kraken/base' +{ Graph, GraphList, GraphDisplayView, +} = require 'kraken/graph' +{ Dashboard, +} = require 'kraken/dashboard/dashboard-model' + + +/** + * @class + */ +DashboardView = exports.DashboardView = BaseView.extend do # {{{ + __bind__ : <[ addTab ]> + tagName : 'section' + className : 'dashboard' + template : require 'kraken/template/dashboard/dashboard' + + + events: + # Select the whole permalink URI text when it receives focus. + 'click .graphs.tabbable .nav a' : 'onTabClick' + 'shown .graphs.tabbable .nav a' : 'render' + # 'shown .graphs.tabbable .nav a' : 'onTabShown' + # 'click a[data-target="#other-graphs"]' : 'onTabShown' + # 'click .load-button' : 'load' + + # subviews : [] + graphs : null + ready : false + + + constructor: function DashboardView(options={}) + @graphs = new GraphList + BaseView ... + + initialize: -> + @model or= new Dashboard + DashboardView.__super__.initialize ... + # @graphs.on 'add', @attachGraphs, this + # @graphs.on 'add', @attachGraph, this + @model.once 'ready', @load, this .load() + + + # FIXME: + # - combine all loads into one seq so... + # - trigger ready when finished + # TODO: + # - only render graph when scrolling makes it visible + load: -> + console.log "#this.load! Model ready!", @model + Seq @model.get('tabs') + .seqEach_ @addTab + .seq ~> + console.log "#{this}.load! Done adding tabs!" + @triggerReady() + + addTab: (nextTab, tab) -> + # self = this + # a(href="#core-graphs", data-toggle="tab") Core + tabModel = new BaseModel tab + tabView = @addSubview new DashboardTabView {model:tabModel} + tabId = tabView.getTabId() + @$ "nav > ul.nav" + .append "
  • #{tab.name}
  • " + + graphs = _(tab.graph_ids).map (graph_id) ~> @model.graphs.get graph_id + Seq graphs + .parMap_ (next, graph) ~> + @graphs.add graph + next null, new GraphDisplayView {model:graph} + .parMap_ (next, graphView) ~> + return next.ok() if graphView.isAttached + tabView.addSubview graphView + # tabEl = @$ tab.name + # if tabEl.length + # tabEl.append view.$el + # view.isAttached = true + # else + # console.log "#this.addTab: Unable to find bind-point for #view!", view + next.ok() + .seq ~> + console.log "#{this}.addTab: All graphs added!" + # @render() + nextTab.ok() + this + + ### Tabs {{{ + + onTabShown: (e) -> + @render() + # @renderSubviews() + # Seq @subviews + # .parMap (view) -> + # # view.resizeViewport() + # view.renderChart() + + onTabClick: (evt) -> + evt.preventDefault() + + + ### }}} + ### Navigation Between Graphs {{{ + + /** + * Scroll to the specified graph. + * + * @param {String|Number|Graph} graph The Graph to scroll to; can be specified as a + * Graph id, an index into the Graphs list, or a Graph object. + * @returns {this} + */ + scrollToGraph: (graph) -> + if typeof graph is 'string' + graph = @graphs.get graph + else if typeof graph is 'number' + graph = @graphs.at graph + unless graph instanceof Graph + console.error "#this.scrollToGraph() Unknown graph #graph!" + return this + + return this unless view = _.find @subviews, -> it.model is graph + $ 'body' .scrollTop view.$el.offset().top if view.$el.is ':visible' + + this + + findClosestGraph: (scroll) -> + scroll or= $ 'body' .scrollTop() + views = @subviews + .filter -> it.$el.is ':visible' + .map -> [ it.$el.offset().top, it ] + .filter -> it[0] >= scroll + .sort (a,b) -> op.cmp a[0], b[0] + return views[0][1] if views.length + + ### }}} + + + +/** + * @class + * @extends BaseView + */ +DashboardTabView = exports.DashboardTabView = BaseView.extend do # {{{ + __bind__ : <[ ]> + className : 'tab-pane' + tag : 'div' + template : require 'kraken/template/dashboard/dashboard-tab' + + + constructor: function DashboardTabView + BaseView ... + + initialize: -> + BaseView::initialize ... + + getTabId: -> + _.underscored @model.get('name') .toLowerCase() + '-graphs-tab' + + toTemplateLocals: -> + json = DashboardTabView.__super__.toTemplateLocals ... + tab_name = _.underscored @model.get('name') .toLowerCase() + json import + tab_cls : "#tab_name-graphs-pane" + tab_id : "#tab_name-graphs-tab" + +# }}} + + + + + diff --git a/src/dashboard/index.co b/src/dashboard/index.co new file mode 100644 index 0000000..03fc595 --- /dev/null +++ b/src/dashboard/index.co @@ -0,0 +1,3 @@ +models = require 'kraken/dashboard/dashboard-model' +views = require 'kraken/dashboard/dashboard-view' +exports import models import views diff --git a/src/data/data-view.co b/src/data/data-view.co new file mode 100644 index 0000000..c57eaa8 --- /dev/null +++ b/src/data/data-view.co @@ -0,0 +1,122 @@ +Seq = require 'seq' +{ _, op, +} = require 'kraken/util' +{ BaseView, ViewList, +} = require 'kraken/base' +{ DataSetView, +} = require 'kraken/data/dataset-view' +{ MetricEditView, +} = require 'kraken/data/metric-edit-view' +{ DataSource, +} = require 'kraken/data/datasource-model' + +/** + * @class DataSet selection and customization UI (root of the `data` tab). + */ +DataView = exports.DataView = BaseView.extend do # {{{ + __bind__ : <[ onMetricsChanged ]> + tagName : 'section' + className : 'data-ui' + template : require 'kraken/template/data/data' + + datasources : null + + + /** + * @constructor + */ + constructor: function DataView + BaseView ... + + initialize: -> + @graph_id = @options.graph_id + BaseView::initialize ... + @metric_views = new ViewList + @datasources = DataSource.getAllSources() + # @on 'update', @onUpdate, this + @model.metrics + .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 + @dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources} + @addSubview @dataset_view + .on 'add-metric', @onMetricsChanged, this + .on 'remove-metric', @onMetricsChanged, this + .on 'select-metric', @selectMetric, this + + @render() + @triggerReady() + this + + + /** + * Transform the `columns` field to ensure an Array of {label, type} objects. + */ + canonicalizeDataSource: (ds) -> + ds.shortName or= ds.name + ds.title or= ds.name + ds.subtitle or= '' + + cols = ds.columns + if _.isArray cols + ds.metrics = _.map cols, (col, idx) -> + if _.isArray col + [label, type] = col + {idx, label, type or 'int'} + else + col + else + ds.metrics = _.map cols.labels, (label, idx) -> + {idx, label, type:cols.types[idx] or 'int'} + ds + + + toTemplateLocals: -> + attrs = _.clone @model.attributes + { @graph_id, @datasources } import attrs + + 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 + .on 'metric-change', @onUpdateMetric, this + @metric_views.push @addSubview view + @renderSubviews() + metric + + removeMetric: (metric) -> + # console.log "#this.removeMetric!", metric + return unless view = @metric_views.findByModel metric + @metric_views.remove view + @removeSubview view + metric + + selectMetric: (metric) -> + # console.log "#this.selectMetric!", metric + @metric_views.invoke 'hide' + @metric_edit_view = @metric_views.findByModel metric + @metric_edit_view?.show() + _.delay @onMetricsChanged, 10 + + onMetricsChanged: -> + return unless @dataset_view + oldMinHeight = parseInt @$el.css 'min-height' + newMinHeight = Math.max do + @dataset_view.$el.height() + @metric_edit_view?.$el.height() + # console.log 'onMetricsChanged!', oldMinHeight, '-->', newMinHeight + @$el.css 'min-height', newMinHeight + + onUpdateMetric: -> + # console.log "#this.onUpdateMetric!" + @trigger 'metric-change', @model, this + @render() + + +# }}} diff --git a/src/data/dataset-model.co b/src/data/dataset-model.co new file mode 100644 index 0000000..5bc6c25 --- /dev/null +++ b/src/data/dataset-model.co @@ -0,0 +1,183 @@ +Seq = require 'seq' +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ BaseModel, BaseList, +} = require 'kraken/base' +{ Metric, MetricList, +} = require 'kraken/data/metric-model' +{ DataSource, DataSourceList, +} = require 'kraken/data/datasource-model' + + + +/** + * @class + */ +DataSet = exports.DataSet = BaseModel.extend do # {{{ + urlRoot : '/datasets' + + /** + * @type DataSourceList + */ + sources : null + + /** + * @type MetricList + */ + metrics : null + + defaults : -> + palette : null + lines : [] + metrics : [] + + + constructor: function DataSet (attributes={}, opts) + @metrics = new MetricList attributes.metrics + BaseModel.call this, attributes, opts + + initialize : -> + BaseModel::initialize ... + @set 'metrics', @metrics, {+silent} + @on 'change:metrics', @onMetricChange, this + # @metrics.on 'add remove reset', ~> + # @trigger 'change:metrics', @metrics, this + + + load: (opts={}) -> + @resetReady() if opts.force + return this if @loading or @ready + + unless @metrics.length + return @triggerReady() + + # console.log "#this.load()..." + @wait() + @loading = true + @trigger 'load', this + Seq @metrics.models + .parEach_ (next, metric) -> + metric.once 'ready', next.ok .load() + .seq ~> + # console.log "#{this}.load() complete!" + @loading = false + @unwait() # terminates the `load` wait + @triggerReady() + this + + # refreshSubModels: -> + # # @set 'metrics', @metrics.toJSON(), {+silent} + # @set 'metrics', _.pluck(@metrics.models, 'attributes'), {+silent} + # this + + /** + * Override to handle the case where one of our rich sub-objects + * (basically `metrics`) is set as a result of the `fetch()` call by the + * Graph object. To prevent it from blowing away the `MetricList`, we + * perform a `reset()` here. But that won't trigger a `change:metrics` event, + * so we do a little dance to set it twice, as object identity would otherwise + * cause it to think nothing has changed. + */ + set: (key, value, opts) -> + # return DataSet.__super__.set ... unless @metrics + + if _.isObject(key) and key? + [values, opts] = [key, value] + else + values = { "#key": value } + opts or= {} + + for key, value in values + continue unless key is 'metrics' and _.isArray value + @metrics.reset value + delete values[key] + unless opts.silent + DataSet.__super__.set.call this, 'metrics', value, {+silent} + DataSet.__super__.set.call this, 'metrics', @metrics, opts + + DataSet.__super__.set.call this, values, opts + + + toJSON: -> + json = DataSet.__super__.toJSON ... + delete json.id + json + + + /* * * * TimeSeriesData interface * * * {{{ */ + + /** + * @returns {Array} The reified dataset, materialized to a list of rows including timestamps. + */ + getData: -> + return [] unless @ready + columns = @getColumns() + if columns?.length + _.zip ...columns + else + [] + + /** + * @returns {Array} List of all columns (including date column). + */ + getColumns: -> + return [] unless @ready + _.compact [ @getDateColumn() ].concat @getDataColumns() + + /** + * @returns {Array} The date column. + */ + getDateColumn: -> + return [] unless @ready + dates = @metrics.onlyOk().invoke 'getDateColumn' + maxLen = _.max _.pluck dates, 'length' + _.find dates, -> it.length is maxLen + + /** + * @returns {Array} List of all columns except the date column. + */ + getDataColumns: -> + return [] unless @ready + @metrics.onlyOk().invoke 'getData' + + /** + * @returns {Array} List of column labels. + */ + getLabels: -> + return [] unless @ready + [ 'Date' ].concat @metrics.onlyOk().invoke 'getLabel' + + getColors: -> + return [] unless @ready + @metrics.onlyOk().invoke 'getColor' + + # }}} + + + newMetric: -> + index = @metrics.length + @metrics.add m = new Metric { index, color:ColorBrewer.Spectral[11][index] } + 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}" + @resetReady() + @load() + + + # XXX: toJSON() must ensure columns in MetricList are ordered by index + # ...in theory, MetricList.comparator now does this + + # toJSON: -> + # @refreshSubModels() + # json = DataSet.__super__.toJSON ... + # json.metrics = json.metrics.map -> it.toJSON?() or it + # json + +# }}} + diff --git a/src/data/dataset-view.co b/src/data/dataset-view.co new file mode 100644 index 0000000..4a55a14 --- /dev/null +++ b/src/data/dataset-view.co @@ -0,0 +1,155 @@ +{ _, op, +} = require 'kraken/util' +{ BaseView, +} = require 'kraken/base' + + +/** + * @class + */ +DataSetView = exports.DataSetView = BaseView.extend do # {{{ + tagName : 'section' + className : 'dataset-ui dataset' + template : require 'kraken/template/data/dataset' + + events: + 'click .new-metric-button' : 'onNewMetric' + 'click .delete-metric-button' : 'onDeleteMetric' + 'click .metrics .dataset-metric' : 'selectMetric' + + views_by_cid : {} + active_view : null + + + constructor: function DataSetView + BaseView ... + + initialize: -> + {@graph_id, @datasources, @dataset} = @options + BaseView::initialize ... + @views_by_cid = {} + @model + .on 'ready', @addAllMetrics, this + @model.metrics + .on 'add', @addMetric, this + .on 'remove', @removeMetric, this + .on 'change', @onMetricChange, this + .on 'reset', @addAllMetrics, 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 + @trigger 'add-metric', metric, view, this + @render() + view + + 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] + @trigger 'remove-metric', metric, view, this + view + + addAllMetrics: -> + # console.log "#this.addAllMetrics! --> #{@model.metrics}" + @removeAllSubviews() + @model.metrics.each @addMetric, this + this + + + selectMetric: (metric) -> + if metric instanceof [jQuery.Event, Event] + 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.$ '.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' + +# }}} + + + +/** + * @class + */ +DataSetMetricView = exports.DataSetMetricView = BaseView.extend do # {{{ + tagName : 'tr' + className : 'dataset-metric metric' + template : require 'kraken/template/data/dataset-metric' + + + + constructor: function DataSetMetricView + BaseView ... + + initialize: -> + @graph_id = @options.graph_id + BaseView::initialize ... + @on 'update', @onUpdate, this + + + toTemplateLocals: -> + m = DataSetMetricView.__super__.toTemplateLocals ... + + # XXX: Icons/classes for visible/disabled? + m import + graph_id : @graph_id + label : @model.getLabel() + viewClasses : _.compact([ + if @model.isOk() then 'valid' else 'invalid', + if m.visible then 'visible' else 'hidden', + 'disabled' if m.disabled, + ]).map( -> "metric-#it" ).join ' ' + source : + if m.source_id and m.source_col + "#{m.source_id}[#{m.source_col}]" + else + 'No source' + timespan : + if _.every ts = m.timespan, op.ok + "#{ts.start} to #{ts.end} by #{ts.step}" + else + '—' + + onUpdate: -> + @$ '.col-color' .css 'color', @model.get 'color' + +# }}} + diff --git a/src/data/datasource-model.co b/src/data/datasource-model.co new file mode 100644 index 0000000..544af94 --- /dev/null +++ b/src/data/datasource-model.co @@ -0,0 +1,190 @@ +{ _, op, +} = require 'kraken/util' +{ TimeSeriesData, CSVData, +} = require 'kraken/util/timeseries' +{ BaseModel, BaseList, ModelCache, +} = require 'kraken/base' +{ Metric, MetricList, +} = require 'kraken/data/metric-model' + + +/** + * @class + */ +DataSource = exports.DataSource = BaseModel.extend do # {{{ + __bind__ : <[ onLoadDataSuccess onLoadDataError ]> + urlRoot : '/datasources' + ready : false + + /** + * Parsed data for this datasource. + * @type Array + */ + data : null + + defaults: -> + id : '' + url : '' + format : 'json' + + name : '' + shortName : '' + title : '' + subtitle : '' + desc : '' + notes : '' + + timespan : + start : null + end : null + step : '1mo' + + columns : [] + + chart : + chartType : 'dygraphs' + options : {} + + url: -> + "/datasources/#{@id}.json" + + + + + + constructor: function DataSource + BaseModel ... + + initialize: -> + @attributes = @canonicalize @attributes + BaseModel::initialize ... + @constructor.register this + @metrics = new MetricList @attributes.metrics + @on 'change:metrics', @onMetricChange, this + + + canonicalize: (ds) -> + ds.shortName or= ds.name + ds.title or= ds.name + ds.subtitle or= '' + + cols = ds.columns + if _.isArray cols + ds.metrics = _.map cols, (col, idx) -> + if _.isArray col + [label, type] = col + {idx, label, type or 'int'} + else + col.type or= 'int' + col + else + ds.metrics = _.map cols.labels, (label, idx) -> + {idx, label, type:cols.types[idx] or 'int'} + ds + + + + 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' then @loadJSON() + case 'csv' then @loadCSV() + default + console.error "#this.load() Unknown Data Format!" + @onLoadDataError null, 'Unknown Data Format!', new Error 'Unknown Data Format!' + this + + loadJSON: -> + $.ajax do + url : @get 'url' + dataType : 'json' + success : (data) ~> @onLoadDataSuccess new TimeSeriesData data + error : @onLoadDataError + this + + loadCSV: -> + $.ajax do + url : @get 'url' + dataType : 'text' + success : (data) ~> @onLoadDataSuccess new CSVData data + error : @onLoadDataError + this + + onLoadDataSuccess: (@data) -> + # 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 ''}" + @unwait() + @_errorLoading = true + @trigger 'load-data-error', this, txtStatus, err + + + getDateColumn: -> + @data?.dateColumn + + getData: -> + @data?.toJSON?() or @data + + getColumn: (idx) -> + @data?.columns[idx] + + getColumnName: (idx) -> + @get('metrics')?[idx]?.label + + getColumnIndex: (name) -> + return that.idx if _.find @get('metrics'), -> it.label is name + -1 + + onMetricChange: -> + @metrics.reset @get 'metrics' + + +# }}} + + +/** + * @class + */ +DataSourceList = exports.DataSourceList = BaseList.extend do # {{{ + urlRoot : '/datasources' + model : DataSource + + constructor: function DataSourceList then BaseList ... + initialize : -> BaseList::initialize ... +# }}} + + + +### DataSource Cache + +ALL_SOURCES = new DataSourceList +sourceCache = new ModelCache DataSource, {-ready, cache:ALL_SOURCES} + +# Fetch all DataSources +$.getJSON '/datasources/all', (data) -> + ALL_SOURCES.reset _.map data, op.I + sourceCache.triggerReady() + +DataSource.getAllSources = -> + ALL_SOURCES + + diff --git a/src/data/datasource-ui-view.co b/src/data/datasource-ui-view.co new file mode 100644 index 0000000..7f04041 --- /dev/null +++ b/src/data/datasource-ui-view.co @@ -0,0 +1,69 @@ +{ _, op, +} = require 'kraken/util' +{ BaseModel, BaseList, BaseView, +} = require 'kraken/base' + + +/** + * @class + * Model is a Metric. + */ +DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{ + __bind__ : <[ ]> + tagName : 'section' + className : 'datasource-ui' + template : require 'kraken/template/data/datasource-ui' + + events : + 'click .datasource-summary' : 'onHeaderClick' + 'click .datasource-source-metric' : 'onSelectMetric' + + graph_id : null + dataset : null + datasources : null + + + + constructor: function DataSourceUIView + BaseView ... + + initialize: -> + this import @options.{graph_id, dataset, datasources} + BaseView::initialize ... + + toTemplateLocals: -> + locals = @model.toJSON() + locals import {@graph_id, @dataset, @datasources, cid:@model.cid} + + ds = @model.source + hasSource = @model.get('source_id')? and ds + locals.source_summary = unless hasSource then '' else @model.getSourceColumnName() + + dsts = ds?.get('timespan') or {} + ts = locals.timespan = _.defaults _.clone(@model.get('timespan')), dsts + hasTimespan = hasMetric and ts.start and ts.end and ts.step + locals.timespan_summary = unless hasTimespan then ' + p.help-block A description of the graph. + + .graph-data-pane.tab-pane(id="#{graph_id}-tab-data", data-subview="DataView") + //- + .row-fluid + label.dataset.control-label(for="#{graph_id}_dataset") Data Set + input.span3.dataset(type='text', id="#{graph_id}_dataset", name='dataset', placeholder='URL to dataset file', value=dataset) + p.help-block This dataset filename will soon be replaced by a friendly UI. + + .graph-options-pane.tab-pane(id="#{graph_id}-tab-options", data-subview="ChartOptionScaffold") + diff --git a/src/template/graph/graph-list.jade b/src/template/graph/graph-list.jade new file mode 100644 index 0000000..ca8951d --- /dev/null +++ b/src/template/graph/graph-list.jade @@ -0,0 +1,7 @@ +section#graph-list + .page-header + h1 Saved Graphs + ul + for graph in collection.models + li + a(href="#{graph.toLink()}") #{graph.get('name')} diff --git a/src/util/aliasdict.co b/src/util/aliasdict.co new file mode 100644 index 0000000..4df93b6 --- /dev/null +++ b/src/util/aliasdict.co @@ -0,0 +1,158 @@ +_ = require 'kraken/util/underscore' + +/** + * @class A mapping of key-value pairs supporting key-aliases. + */ +class AliasDict + + /** + * Data store. + * @type Object + * @private + */ + _data : null + + /** + * Mapping from keys to an array of [potentially nested] alias-keys. + * @type Object> + * @private + */ + _aliases : null + + + /** + * @constructor + */ + -> + @_data = {} + @_aliases = {} + @extend ... + + + /** + * @returns {Number} Number of real keys in the Dict. + */ + size : -> + _.keys @_data .length + + /** + * @returns {AliasDict} A copy of the AliasDict, including aliases as well as data. + */ + clone: -> + d = new AliasDict @_data + _.each @_aliases, (v, k) -> + d.setAlias k, v.slice() + d + + + + ### Value Accessors ### + + /** + * @returns {Boolean} Whether there is a value at the given key. + */ + has : (key) -> + (@get key)? + + /** + * @returns {*} Ignores aliases, returning the value at key or `undefined`. + */ + getValue : (key) -> + prop = _.getNested @_data, key + prop.value if prop? + + get : (key, def) -> + aliases = @_aliases[key] or [key] + val = aliases.reduce do + (val, alias) -> + return val if val? is not undefined + prop = _.getNested @_data, alias + prop.value if prop? + undefined + + if val is not undefined + val + else + def + + set : (key, val) -> + _.setNested @_data, key, val, {+ensure} + val + + del : (key) -> + prop = _.getNestedMeta key + if prop + delete prop.obj[prop.key] + prop.value + + + + ### Alias Methods ### + + hasAlias : (key) -> + @_aliases[key]? + + getAlias : (key, def=[]) -> + @_aliases[key] or def + + setAlias: (key, aliases) -> + @_aliases[key] = if _.isArray aliases then aliases else [aliases] + this + + addAlias : (key, ...aliases) -> + @_aliases[key] = _.flatten @getAlias(key, [key]).concat(aliases) + this + + delAlias : (key) -> + delete @_aliases[key] + + + + ### Collection Methods ### + + toObject: -> + _.clone @_data + + keys: -> + _.keys @_data + + values: -> + _.values @_data + + extend : (...args) -> + for o of args + for k,v in o then @set k, v + this + + reduce : (fn, acc, context=this) -> + _.reduce @_data, fn, acc, context + + map : (fn, context=this) -> + _.map @_data, fn, context + + filter: (fn, context=this) -> + _.filter @_data, fn, context + + each : (fn, context=this) -> + _.each @_data, fn, context + this + + invoke : (name, ...args) -> + _.invoke @_data, name, ...args + + pluck : (attr) -> + _.pluck @_data, attr + + find: (fn, context=this) -> + _.find @_data, fn, context + + + toString: -> + Cls = @.constructor + "#{Cls.displayName or Cls.name}()" + + +module.exports = exports = AliasDict + + + diff --git a/src/util/backbone.co b/src/util/backbone.co new file mode 100644 index 0000000..e3d5d28 --- /dev/null +++ b/src/util/backbone.co @@ -0,0 +1,128 @@ +### Patches to make Backbone work with browserify + +# Expose Underscore so Backbone can find it +_ = require 'underscore' +window?._ = _ + +# Expose Backbone so plugins can find it +Backbone = require 'backbone' +window?.Backbone = Backbone + +# Pass jQuery to Backbone, as it only looks on its module if `require` is defined +Backbone.setDomLibrary that if window? and (window.jQuery or window.Zepto or window.ender) + + +_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 + this + + /** + * Compatibility with Node's `EventEmitter`. + */ + emit: Backbone.Events.trigger + + + +/** + * @namespace Meta-utilities for working with Backbone classes. + */ +_backbone = do + + # /** + # * Mix the given object or prototype into this class + # */ + # addMixin: (mixin) -> + # ... + + /** + * @returns {Array} The list of all superclasses for this class or object. + */ + getSuperClasses: function getSuperClasses(Cls) + return [] unless Cls + + if Cls.__superclass__ + superclass = that + else + 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 + else + [] + + /** + * Looks up an attribute on the prototype of each class in the class + * hierarchy. + * @returns {Array} + */ + pluckSuper: (obj, prop) -> + return [] unless obj + _ _backbone.getSuperClasses(obj) .chain() + .pluck 'prototype' + .pluck prop + .value() + + /** + * As `.pluckSuper()` but includes value of `prop` on passed `obj`. + * @returns {Array} + */ + pluckSuperAndSelf: (obj, prop) -> + return [] unless obj + [ obj[prop] ].concat _backbone.pluckSuper(obj, prop) + + +exports import _backbone + + + +/** + * Decorates a function so that its receiver (`this`) is always added as the + * first argument, followed by the call arguments. + * @returns {Function} + */ +methodize = exports.methodize = (fn) -> + m = fn.__methodized__ + return m if m + + g = fn.__genericized__ + return that if g?.__wraps__ + + m = fn.__methodized__ = (...args) -> + args.unshift this + fn.apply this, args + + m.__wraps__ = fn + m + + +# Add the class extensions as static methods of the Backbone classes +_methodized = exports._methodized = _.reduce do + _backbone + (o, v, k) -> + o[k] = if typeof v is 'function' then methodize v else v + o + {} + + +_.extend Backbone.Events, _bb_events +for Cls of Backbone.<[ Model Collection View ]> + Cls import _methodized import _bb_events import Backbone.Events + Cls:: import _methodized import _bb_events + diff --git a/src/util/bitstring.co b/src/util/bitstring.co new file mode 100644 index 0000000..a5be754 --- /dev/null +++ b/src/util/bitstring.co @@ -0,0 +1,247 @@ +SEEK_ABSOLUTE = 0 +SEEK_RELATIVE = 1 +SEEK_FROM_EOF = 2 + + +# Binary representation of the number +bin = (n) -> + do + s = (if n % 2 then '1' else '0') + (s or '') + n >>= 1 + while n + s + +# Number of bits needed to represent the absolute value of n. +binlen = (n) -> + bin Math.abs n .length + +# Returns a run of 1s of size n. +mask = (n) -> + (1 << n) - 1 + +chr = -> String.fromCharCode it +ord = -> String(it).charCodeAt 0 + + + + +/** + * File-like object for reading/writing bits. + * @class + */ +class BitString + # Array + buf : null + + # Byte position of read/write cursor (-1 for end). + _pos : -1 + + # Spill cache for bits smaller than a byte waiting to write. + _spill : 0 + + # Number of bits in the spill cache. + _spillen : 0 + + # Peek cache for read requests smaller than a byte. + _peek : 0 + + # Number of bits in the peek cache. + _peeklen : 0 + + + + (source='', buf=[]) -> + @buf = buf.slice() + for i til source.length + @_bufwrite source.charCodeAt i + + size: -> + @buf.length + if @_spillen then 1 else 0 + + bitsize: -> + @buf.length*8 + @_spillen + + _bufwrite: (b) -> + if @_pos is -1 + @buf.push b + else + @buf[@_pos] = b + @_pos = -1 if ++@_pos >= @buf.length + this + + # Writes bits to the stream; bits must be supplied as a number. Supplying n=0 will write one bit. + # Supplying the optional parameter length treats the bits as a field with the given length. + writebits: (n, size) -> + size = size or binlen n + bits = (@_spill << size) | n + size += @_spillen # handles _spill=0 but _spillen > 0 + while size >= 8 + size -= 8 + b = bits >> size + bits &= mask size + @_bufwrite b + @_spill = bits + @_spillen = size + this + + # Flushes any pending bits to the stream. + flush: -> + b = @_spill + if @_spillen + b <<= 8 - @_spillen + @_bufwrite b + @_spill = 0 + @_spillen = 0 + this + + # Truncates the stream to zero bits. + truncate: -> + @buf = [] + @_pos = -1 + @_spill = 0 + @_spillen = 0 + @_peek = 0 + @_peeklen = 0 + this + + # Move buffer cursor to given byte-offset. mode: 0 = absolute, 1 = relative, 2 = relative EOF + _bufseek: (n, mode=SEEK_ABSOLUTE) -> + switch mode + case 1 # relative + pos = @_pos + n + case 2 + pos = @buf.length + n + default # absolute + pos = n + @_pos = if pos >= @buf.length then -1 else Math.max 0, pos + this + + # Flushes the bit-buffer and moves to the given byte-offset. mode: 0 = absolute, 1 = relative, 2 = relative EOF + seek: (n, mode=SEEK_ABSOLUTE) -> + @flush() + @_peek = 0 + @_peeklen = 0 + @_bufseek n, mode + this + + # Returns the current position of the cursor as a *byte* offset from the start of the stream. + tell: -> + if @_pos is -1 then @buf.length else @_pos + + + _nextbyte: -> + return null if @_pos is -1 + byte = @buf[ @_pos++ ] + @_pos = -1 if @_pos >= @buf.length + byte + + + # Reads n bits from the stream. + readbits: (n) -> + return 0 if n == 0 + + size = @_peeklen + bits = @_peek + + while size < n + byte = @_nextbyte() + break unless byte? + size += 8 + bits = (bits << 8) | byte + + if size > n + @_peeklen = size - n + @_peek = bits & mask(@_peeklen) + bits >>= @_peeklen + else + @_peeklen = 0 + @_peek = 0 + + return if size then bits else null + + + # Reads the next n bits without moving the cursor. + peek: (n) -> + offset = 0 + size = @_peeklen + bits = @_peek + + while size < n + byte = @_nextbyte() + break unless byte? + offset += 1 + size += 8 + bits = (bits << 8) | byte + + if size == 0 + return null + + if size > n + bits >>= size - n + + if offset + @_bufseek -offset, SEEK_RELATIVE + bits + + + # True if there is more data to read. + hasMore: -> + @peek(1)? + + + ### XXX: Should .each(), .map(), .reduce() flush? + + # forEach of bytes + each: (fn, cxt=this) -> + @buf.forEach fn, cxt + + # map over bytes + map: (fn, cxt=this) -> + @buf.map fn, cxt + + # reduce over bytes + reduce: (fn, acc, cxt=this) -> + fn .= bind this + @buf.reduce fn, acc + + + # Returns the stream as a bytearray. + bytearray: -> + @flush().buf.slice() + + # Dumps the stream as a binary string. Unlike __index__(), bin() will not cause int overflow. + bin: (byte_sep='') -> + @flush().buf.map(bin).join(byte_sep) + + # Returns the stream as a hex string. + hex: -> + @flush().buf.map(hex).join('') + + # Returns the buffer as a number. Use this with obvious caution. Called by builtins bin(), int(), long(), etc. + number: -> + @flush() + @reduce (n, byte) -> (n << 8) | byte + + + # Dumps the stream as a string; does not flush or change cursor position. + dump: -> + @buf.map(chr).join('') + if @_spillen then chr @_spill << (8 - @_spillen) else '' + + repr: (dump_buf=true) -> + s = if dump_buf then "buf=#{@dump()}" else "len(buf)=#{@buf.length}" + return "BitString(#s, + spill[#{@_spillen}]=#{bin @_spill}, + tell=#{@tell()}, + peek[#{@_peeklen}]=#{bin @_peek}) + " + + # Dumps the stream as a string; flushes the bit-buffer but leaves cursor position unchanged. + toString: -> + @flush().dump() + + +exports = module.exports = BitString + +exports.SEEK_ABSOLUTE = SEEK_ABSOLUTE +exports.SEEK_RELATIVE = SEEK_RELATIVE +exports.SEEK_FROM_EOF = SEEK_FROM_EOF diff --git a/src/util/cascade.co b/src/util/cascade.co new file mode 100644 index 0000000..1c09368 --- /dev/null +++ b/src/util/cascade.co @@ -0,0 +1,379 @@ +_ = require 'kraken/util/underscore' + +hasOwn = ({}).hasOwnProperty + +/** + * Sentinel for missing values. + */ +MISSING = void + +/** + * Tombstone for deleted, non-passthrough keys. + */ +TOMBSTONE = {} + + +/** + * @class A mapping of key-value pairs supporting lookup fallback across multiple objects. + */ +class Cascade + /** + * Sentinel tombstone for deleted, non-passthrough keys. + * @type TOMBSTONE + * @readonly + */ + @TOMBSTONE = TOMBSTONE + + + /** + * Map holding the object's KV-pairs; always the second element of the + * cascade lookup. + * @type Object + * @private + */ + _data : null + + /** + * Map of tombstones, marking intentionally unset keys in the object's + * KV-pairs; always the first element of the cascade lookup. + * @type Object + * @private + */ + _tombstones : null + + /** + * List of objects for lookups. + * @type Array + * @private + */ + _lookups : null + + + + /** + * @constructor + */ + (data={}, lookups=[], tombstones={}) -> + @_data = data + @_tombstones = tombstones + @_lookups = [@_data].concat lookups + + + /** + * @returns {Cascade} A copy of the data and lookup chain. + */ + clone: -> + new Cascade do + {} import @_data + @_lookups.slice() + {} import @_tombstones + + + + ### Data & Lookups ### + + getData: -> + @_data + + setData: (data) -> + @_data = @_lookups[0] = data + this + + getTombstones: -> + @_tombstones + + # setTombstones: (tombstones) -> + # @_tombstones = tombstones + # for k, v in _.collapseObject @_tombstones + # if v + # _.setNested @_tombstones, k, TOMBSTONE, {+ensure} + # else + # _.unsetNested @_tombstones, k + # this + + /** + * @returns {Number} Number of lookup dictionaries. + */ + size: -> + @_lookups.length - 1 + + /** + * @returns {Array} The array of lookup dictionaries. + */ + getLookups: -> + @_lookups + + /** + * @returns {Array} The array of lookup dictionaries. + */ + getLookups: -> + @_lookups + + /** + * Adds a new lookup dictionary to the chain. + * @returns {this} + */ + addLookup: (dict) -> + return this unless dict? + throw new Error "Lookup dictionary must be an object! dict=#dict" unless _.isObject dict + @_lookups.push dict + this + + /** + * Removes a lookup dictionary from the chain (but will not remove the data object). + * @returns {this} + */ + removeLookup: (dict) -> + _.remove @_lookups, dict if dict and dict is not @_data + this + + /** + * Pops the last dictionary off the lookup chain and returns it. + * @returns {*} The last dictionary, or `undefined` if there are no additional lookups. + */ + popLookup: -> + return if @size() <= 1 + @_lookups.pop() + + /** + * Shifts the first additional lookup dictionary off the chain and returns it. + * @returns {*} The first dictionary, or `undefined` if there are no additional lookups. + */ + shiftLookup: -> + return if @size() <= 1 + @_lookups.splice(1, 1)[0] + + /** + * Adds a lookup dictionary to the front of the chain, just after the Cascade's own data + * object. + * @returns {this} + */ + unshiftLookup: (dict) -> + return this unless dict? + throw new Error "Lookup dictionary must be an object! dict=#dict" unless _.isObject dict + @_lookups.splice 1, 0, dict + this + + + /** + * @returns {Boolean} Whether there is a tombstone set for `key`. + */ + hasTombstone: (key) -> + o = @_tombstones + for part of key.split('.') + o = o[part] + return true if o is TOMBSTONE + return false unless o + false + + /** + * @returns {Boolean} Whether `key` belongs to this object (not inherited + * from the cascade). + */ + isOwnProperty: (key) -> + return true if @hasTombstone key + meta = _.getNestedMeta(@_data, key) + meta?.obj and hasOwn.call meta.obj, key + + /** + * @returns {Boolean} Whether `key` belongs to this object (not inherited + * from the cascade) and is defined. + */ + isOwnValue: (key) -> + not @hasTombstone key + and @isOwnProperty key + and _.getNested(@_data, key, MISSING) is not MISSING + + /** + * @returns {Boolean} Whether the value at `key` is the same as that + * inherited by from the cascade. + */ + isInheritedValue: (key, strict=false) -> + return false if @hasTombstone key + val = @get key + cVal = @_getInCascade key, MISSING, 2 + if strict + val is cVal + else + _.isEqual val, cVal + + /** + * @returns {Boolean} Whether the value at `key` is different from that + * inherited by from the cascade. + */ + isModifiedValue: (key, strict=false) -> + not @isInheritedValue key, strict + + + + ### Value Accessors ### + + /** + * @private + * @param {String} key Key to look up. + * @param {*} [def=undefined] Value to return if lookup fails. + * @param {Number} [idx=0] Index into lookup list to begin search. + * @returns {*} First value for `key` found in the lookup chain starting at `idx`, + * and `def` otherwise. + */ + _getInCascade : (key, def, idx=0) -> + return def if @hasTombstone key + + lookups = if idx then @_lookups.slice(idx) else @_lookups + for data of lookups + val = _.getNested data, key, MISSING, {tombstone:TOMBSTONE} + return def if val is TOMBSTONE + return val unless val is MISSING + def + + /** + * @returns {Boolean} Whether there is a value at the given key. + */ + has : (key) -> + @get(key, MISSING) is not MISSING + + /** + * @param {String} key Key to look up. + * @param {*} [def=undefined] Value to return if lookup fails. + * @returns {*} First value for `key` found in the lookup chain, + * and `def` otherwise. + */ + get : (key, def) -> + @_getInCascade key, def + + /** + * Sets a key to a value, accepting nested keys and creating intermediary objects as necessary. + * @public + * @name set + * @param {String} key Key to set. + * @param {*} value Non-`undefined` value to set. + * @returns {this} + */ + /** + * @public + * @name set + * @param {Object} values Map of pairs to set. No value may be `undefined`. + * @returns {this} + */ + set : (values) -> + # Handle @set(k, val) + if arguments.length > 1 and typeof values is 'string' + [key, val] = arguments + throw new Error("Value and key cannot be undefined!") if not key or val is void + values = { "#key": val } + + # Set and ensure the creation of missing intermediate objects. + for key, val in values + _.unsetNested @_tombstones, key, {+ensure} + _.setNested @_data, key, val, {+ensure} + + this + + + /** + * Delete the given key from this object's data dictionary and set a tombstone + * which ensures that future lookups do not cascade and thus see the key as + * `undefined`. + * + * If the key is missing from the data dictionary the delete does not cascade, + * but the tombstone is still set. + * + * @param {String} key Key to unset. + * @returns {undefined|*} If found, returns the old value, and otherwise `undefined`. + */ + unset: (key) -> + old = @get key + _.unsetNested @_data, key + _.setNested @_tombstones, key, TOMBSTONE, {+ensure} + old + + + /** + * Unsets the key in the data dictionary, but ensures future lookups also + * see the key as `undefined`, as opposed. + * + * @param {String} key Key to unset. + * @returns {this} + */ + inherit: (key) -> + _.unsetNested @_tombstones, key, {+ensure} + _.unsetNested @_data, key + + + + ### Collection Methods ### + + extend : -> + for o of arguments then @set o + this + + /** + * Recursively collapses the Cascade to a plain object by recursively merging the + * lookups (in reverse order) into the data. + * @returns {Object} + */ + collapse: -> + o = _.merge {}, ...@_lookups.slice(1).reverse() + for k in @_tombstones + delete o[k] + _.merge o, @_data + + /** + * Returns a plain object for JSON serialization via {@link Cascade#collapse()}. + * The name of this method is a bit confusing, as it doesn't actually return a + * JSON string -- but I'm afraid that it's the way that the JavaScript API for + * `JSON.stringify()` works. + * + * @see https://developer.mozilla.org/en/JSON#toJSON()_method + * @return {Object} Plain object for JSON serialization. + */ + toJSON: -> + @collapse() + + # XXX: should unique? but then won't map 1:1 to @values()... + keys: -> + _.flatten _.map @_lookups, -> _.keys it + + values: -> + _.flatten _.map @_lookups, -> _.values it + + reduce : (fn, acc, context=this) -> + _.reduce @_lookups, fn, acc, context + + map : (fn, context=this) -> + _.map @_lookups, fn, context + + filter: (fn, context=this) -> + _.filter @_lookups, fn, context + + each : (fn, context=this) -> + _.each @_lookups, fn, context + this + + invoke : (name, ...args) -> + _.invoke @_lookups, name, ...args + + pluck : (attr) -> + _.pluck @_lookups, attr + + find: (fn, context=this) -> + _.find @_lookups, fn, context + + + + toString: -> + Cls = this.constructor + "#{Cls.displayName or Cls.name}()" + + +# Alias methods to alternate names +ALIASES = + setTombstone : 'unset' + toObject : 'collapse' + forEach : 'each' + +for dest, src in ALIASES + Cascade::[dest] = Cascade::[src] + + +module.exports = exports = Cascade diff --git a/src/util/crc.co b/src/util/crc.co new file mode 100644 index 0000000..056b090 --- /dev/null +++ b/src/util/crc.co @@ -0,0 +1,58 @@ + +crc32 = exports.crc32 = (s, last_crc=0) -> + s = utf8Encode s + crc = last_crc ^ (-1) + for i til s.length + y = (crc ^ s.charCodeAt i) & 0xFF + x = "0x" + TABLE.substr y*9, 8 + crc = (crc >>> 8) ^ x + crc ^ (-1) + + +utf8Encode = exports.utf8Encode = (s) -> + s = s.replace /\r\n/g, '\n' + u = '' + for n til s.length + c = s.charCodeAt n + if c < 128 + u += String.fromCharCode c + else if 127 < c < 2048 + u += String.fromCharCode (c >> 6) | 192 + u += String.fromCharCode (c & 63) | 128 + else + u += String.fromCharCode (c >> 12) | 224 + u += String.fromCharCode ((c >> 6) & 63) | 128 + u += String.fromCharCode (c & 63) | 128 + u + + +# static precompiled hashes +TABLE = ''' + 00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 + E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE + 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 + FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B + 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A + C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 + 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F + 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 + 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 + 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 + 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 + AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F + 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 + 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 + E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB + 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 + D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C + 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 + CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 + 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 + 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 + 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C + 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 + 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 + BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 + 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D +''' + diff --git a/src/util/event/index.co b/src/util/event/index.co new file mode 100644 index 0000000..18c3e40 --- /dev/null +++ b/src/util/event/index.co @@ -0,0 +1,2 @@ +exports.WaitingEmitter = require 'kraken/util/event/waiting-emitter' +exports.ReadyEmitter = require 'kraken/util/event/ready-emitter' diff --git a/src/util/event/ready-emitter.co b/src/util/event/ready-emitter.co new file mode 100644 index 0000000..049ad1c --- /dev/null +++ b/src/util/event/ready-emitter.co @@ -0,0 +1,54 @@ +Base = require 'kraken/base/base' + + +/** + * @class An EventEmitter that auto-triggers new handlers once "ready". + */ +class ReadyEmitter extends Base + readyEventName : 'ready' + ready : false + + /** + * Triggers the 'ready' event if it has not yet been triggered. + * Subsequent listeners added to this event will be auto-triggered. + * @param {Boolean} [force=false] Trigger the event even if already ready. + * @returns {this} + */ + triggerReady: (force) -> + return this if @ready and not force + @ready = true + @emit @readyEventName, this + this + + /** + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @param {Boolean} [force=false] Trigger the event even if already reset. + * @returns {this} + */ + resetReady: (force) -> + return this unless @ready and not force + @ready = false + @emit "#{@readyEventName}-reset", this + this + + + /** + * Wrap {@link EventEmitter#on} registration to handle registrations + * on 'ready' after we've broadcast the event. Handler will always still + * be registered, however, in case the emitter is reset. + * + * @param {String} events Space-separated events for which to register. + * @param {Function} callback + * @param {Object} [context] + * @returns {this} + */ + on: (events, callback, context=this) -> + return this if not callback + super ... + if @ready and -1 is not events.split(/\s+/).indexOf @readyEventName + setTimeout ~> callback.call context, this + this + + +module.exports = exports = ReadyEmitter diff --git a/src/util/event/waiting-emitter.co b/src/util/event/waiting-emitter.co new file mode 100644 index 0000000..3f29ddf --- /dev/null +++ b/src/util/event/waiting-emitter.co @@ -0,0 +1,55 @@ +Base = require 'kraken/base/base' + + +/** + * @class An EventEmitter with a ratchet-up waiting counter. + * @extends Base + */ +class WaitingEmitter extends Base + + /** + * Count of outstanding tasks. + * @type Number + */ + waitingOn : 0 + + + /** + * Increment the waiting task counter. + * @returns {this} + */ + wait: -> + count = @waitingOn + @waitingOn += 1 + # console.log "#this.wait! #count --> #{@waitingOn}" + # console.trace() + @trigger('start-waiting', this) if count is 0 and @waitingOn > 0 + this + + /** + * Decrement the waiting task counter. + * @returns {this} + */ + unwait: -> + count = @waitingOn + @waitingOn -= 1 + # console.warn "#this.unwait! #{@waitingOn} < 0" if @waitingOn < 0 + # console.log "#this.unwait! #count --> #{@waitingOn}" + # console.trace() + @trigger('stop-waiting', this) if @waitingOn is 0 and count > 0 + this + + /** + * @param {Function} fn Function to wrap. + * @returns {Function} A function wrapping the passed function with a call + * to `unwait()`, then delegating with current context and arguments. + */ + unwaitAnd: (fn) -> + self = this + -> + # console.log "#self.unwaitAnd( function #{fn.name or fn.displayName}() )" + # console.trace() + self.unwait(); fn ... + + +module.exports = exports = WaitingEmitter diff --git a/src/util/formatters.co b/src/util/formatters.co new file mode 100644 index 0000000..f2c3b95 --- /dev/null +++ b/src/util/formatters.co @@ -0,0 +1,68 @@ +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. + * @param {Boolean} [abbrev=true] Expand number suffixes if false. + * @returns {Object} Formatted number parts. + */ + numberFormatter: (n, digits=2, abbrev=true) -> + suffixes = do + if abbrev + [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + else + [['Billion', 1000000000], ['Million', 1000000], ['', NaN]] + + for [suffix, d] of suffixes + 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, toString: -> + "#{@whole}#{@fraction}#{if abbrev then '' else ' '}#{@suffix}" + } + + numberFormatterHTML: (n, digits=2) -> + { whole, fraction, suffix } = _fmt._numberFormatter n, digits + # coco will trim the whitespace + " + #whole + #fraction + #suffix + " + + + + +module.exports = exports = _fmt diff --git a/src/util/hashset.co b/src/util/hashset.co new file mode 100644 index 0000000..0c99bef --- /dev/null +++ b/src/util/hashset.co @@ -0,0 +1,418 @@ +_ = require 'underscore' + + +/** + * A Set class, implemented using the `__id__` property on non-primitive objects it is passed. + * Arrays are hashed based on their contents. If an object lacks `__id__`, an exception will be + * thrown. This class does not keep values in sorted order. + * + * Underscore provides an easy way to generate unique IDs with the (surprise!) `_.uniqueId()` + * function. + * @see http://documentcloud.github.com/underscore/#uniqueId + * + * @class + */ +class HashSet + /** + * Objects by Id. + * @private + */ + _byId : {} + + /** + * Set contents. + * @private + */ + _o : [] + + /** + * Number of elements in the set. + * @property {Number} + */ + length : 0 + + + /** + * Accepts any number of collections to be added to the set. + * @constructor + */ + -> + @_byId = {} + @_o = [] + @update ...arguments if arguments.length + + + + /** + * Determine unique identifier for the given value. + * @private + * @returns {String} Id for this value. + */ + _getIdSafe : (v) -> + t = typeof v + + switch t + case 'undefined' + return 'u' + case 'boolean' 'string' 'number' + return "#{t.charAt 0}:#v" + if v is null + return 'n' + if '__id__' in v + return 'o:' + v.__id__ + if _.isArray v + return 'a:' + v.map @_getIdSafe, this .join ',' + + /** + * Determine unique identifier for the given value, throwing an exception otherwise. + * @private + * @returns {String} Id for this value. + */ + _getId : (v) -> + id = @_getIdSafe v + unless id? + throw new Error "HashSet elements must be hashable (#v)" + id + + + + /** + * Aliases: HashSet#has + * @param {Any} v Value to test. + * @returns {Boolean} Whether HashSet contains value. + */ + contains : (v) -> + @_getIdSafe(v) in @_byId + + + /** + * @private + * @returns {this} + */ + _addOne : (v) -> + id = @_getId v + unless id in @_byId + @_byId[id] = v + @_o.push(v) + @length = @_o.length + this + + + /** + * Add values to the HashSet. + * Aliases: HashSet#push HashSet#unshift + * @param {Any} values... Values to add. + * @returns {this} + */ + add : (...values) -> + _.each arguments, @_addOne, this + this + + /** + * @private + * @returns {this} + */ + _removeOne : (v) -> + id = @_getId v + if id in @_byId + delete @_byId[id] + @_o.splice @_o.indexOf(v), 1 + @length = @_o.length + this + + + /** + * Remove values from the HashSet. + * Aliases: HashSet#without + * @param {Any} values... Values to remove. + * @returns {this} + */ + remove : (...values) -> + _.each arguments, @_removeOne, this + this + + + /** + * Update this HashSet (in-place) with other collections. + * Aliases: HashSet#extend HashSet#concat + * @param {Array|Object} it... Collection to add. + * @returns {this} + */ + update : (vs) -> + _.each arguments, ~> _.each it, @_addOne, this + this + + + /** + * Remove and return an element from the set. + * Aliases: HashSet#shift + * @returns {Any} An element from the set. + */ + pop : -> + return unless @_o.length + v = @_o.shift() + id = @_getIdSafe v + delete @_byId[id] + return v + + + /** + * Returns but does not remove the an element from the set. + * @returns {Any} An element from the set. + */ + element : -> + @_o[0] + + + /** + * Clones the set, returning a new object. + * @returns {HashSet} + */ + clone : -> + new HashSet @_o + + + /** + * Removes all elements from the set. + * Aliases: HashSet#empty + * @returns {this} + */ + clear: -> + @_byId = {} + @_o = [] + @length = 0 + this + + + + ### Collection Operations + + /** + * Transforms the collection into a single value, front-to-back. + * Aliases: HashSet#inject HashSet#fold HashSet#foldl HashSet#foldr + * @param {Function} fn Reducer function. + * @param {Any} [acc] Starting accumulator value. + * @param {Object} [cxt=this] Context; defaults to this HashSet. + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduce + * @returns {Any} + */ + reduce: (fn, acc, cxt) -> + _.reduce @_o, fn, acc, cxt or this + + /** + * Applies a function to each element. + * Aliases: HashSet#each + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach + * @returns {this} + */ + forEach: (fn, cxt) -> + _.forEach @_o, fn, cxt or this + this + + /** + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map + * @return {HashSet} A new HashSet of elements produced by applying the transform across each element. + */ + map: (fn, cxt) -> + new HashSet _.map @_o, fn, cxt or this + + + /** + * Aliases: HashSet#select + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter + * @return {HashSet} A new HashSet of only the elements passing the filter. + */ + filter: (fn, cxt) -> + new HashSet _.filter @_o, fn, cxt or this + + /** + * Like `HashSet.filter()`, but instead keeps values for which the filter returns false. + * @see HashSet#filter + * @return {HashSet} A new HashSet of only the elements for which the filter returns false. + */ + reject: (fn, cxt) -> + new HashSet _.reject @_o, fn, cxt or this + + + /** + * Aliases: HashSet#any + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some + * @return {Boolean} + */ + some: (fn, cxt) -> + _.some @_o, fn, cxt or this + + /** + * Aliases: HashSet#all + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every + * @return {Boolean} + */ + every: (fn, cxt) -> + _.every @_o, fn, cxt or this + + + /** + * Iterates through the HashSet, returning the first value for which `fn` returns truth-y. + * Aliases: HashSet#detect + * @returns {Any} + */ + find: (fn, cxt) -> + _.find @_o, fn, cxt or this + + + /** + * @returns {Array} List of all values at property `prop`. + */ + pluck : (prop) -> + _.pluck @_o, prop + + + /** + * Invokes the named method on each element in the set, returning a list of the results. + * @param {String} methodName Name of the method on each element to call. + * @param {Any...} [args...] Optional arguments to call pass to method call. + * @returns {Array} List of results. + */ + invoke : (methodName) -> + _.invoke @_o, ...arguments + + + /** + * @returns {Array} List of the unique identifiers for each element of the set. + */ + keys : -> + _.keys @_byId + + /** + * Converts this HashSet to an Array. + * Aliases: HashSet#toArray + * @returns {Array} + */ + values : -> + @_o.slice() + + + + ### Comparators and HashSet Operations + + /** + * Tests if `a` is a Collection and has all elements in common with the set. + * Sets are equal if and only if their intersection has the same size as both sets. + * @param {Collection} a + * @returns {Boolean} + */ + equals : (a) -> + return false unless a + L = @_o.length + return L is a.length and L is @intersect(a).length + + + /** + * Tests if the set has no elements in common with `a`. + * Sets are disjoint if and only if their intersection is the empty set. + * @param {Collection} a + * @returns {Boolean} + */ + isDisjoint : (a) -> + return true unless a + return not _.some a, @contains, this + + + /** + * Test whether every element in the set is in `a`. + * @param {Collection} a + * @returns {Boolean} + */ + isSubset : (a) -> + return false unless a + A = _ if _.isArray a then a else _.values a + @every A.contains, A + + + /** + * Test whether every element in `a` is in the set. + * @param {Array|Object} a + * @returns {Boolean} + */ + isSuperset : (a) -> + return false unless a + _.every a, @contains, this + + + /** + * HashSet Intersection (A ^ B) + * Intersects this YArray with another collection, returning a new YArray. + * The membership test uses _(a).contains(), so it is possible to intersect collections of different types. + * For YArray and YObject, .contains() uses strict equality (is) via .indexOf(). + * + * @param {Array|Object} a Comparison collection. + * @returns {HashSet} A new YArray of all elements of {this} found in the supplied collection. + * + * @example + * foo = /foo/ + * A = [foo, 'A', 1, 2, 3, 'C', /foo/] + * B = [foo, 'B', 3, 'A', 1, /foo/] + * ins = _(A).intersect(B) + * ins.toString() is "HashSet([/foo/,A,1,3])"; # true + * ins.get(0) is foo; # true + */ + intersect : (a) -> + new HashSet _.intersect @_o, _.map arguments, _.values + + + /** + * HashSet Union (A v B) + * Aliases: HashSet#extend HashSet#concat + * @param {Array|Object} a Other collection(s). + * @returns {HashSet} A new HashSet of all elements of both collections, without duplicates. + */ + union : (a) -> + _.reduce arguments, ((out, it) -> out.update it), @clone() + + + /** + * HashSet Difference (A - B) + * @param {Array|Object} a Comparison collection(s). + * @returns {HashSet} A new HashSet of only elements of this HashSet not in supplied collection(s). + */ + difference : (a) -> + new HashSet _.difference @_o, _.map arguments, _.values + + + /** + * Symmetric Difference (A - B) v (B - A) + * @returns {HashSet} + */ + xor : (a) -> + a = _.values a + @difference a .union _.difference a, @_o + + + toString : -> + "HashSet([#{@_o}])" + + + +### Aliases + +pt = HashSet:: + +pt.push = pt.unshift = pt.add +pt.shift = pt.pop +pt.without = pt.remove +pt.empty = pt.clear +pt.has = pt.include = pt.contains + +pt.fold = pt.foldl = pt.foldr = pt.inject = pt.reduce +pt.each = pt.forEach +pt.select = pt.filter +pt.all = pt.every +pt.any = pt.some + +pt.detect = pt.find +pt.toArray = pt.values +pt.extend = pt.concat = pt.union + + +exports = module.exports = HashSet + diff --git a/src/util/index.co b/src/util/index.co new file mode 100644 index 0000000..ddcacba --- /dev/null +++ b/src/util/index.co @@ -0,0 +1,38 @@ +_ = exports._ = require 'kraken/util/underscore' +op = exports.op = require 'kraken/util/op' + +# Root object -- `window` in the browser, `global` in Node. +root = exports.root = do -> this + +# Stub out console with empty methods +root.console or= _ <[ log info warn error dir table group groupCollapsed groupEnd ]> .synthesize -> [it, op.nop] + +### Extend jQuery with useful functions + +/** + * @returns {Object} Object of the data from the form, via `.serializeArray()`. + */ +root.jQuery?.fn.formData = -> + _.synthesize do + this.serializeArray() + -> [it.name, it.value] + +/** + * Invokes a jQuery method on each element, returning the array of the result. + * @returns {Array} Results. + */ +root.jQuery?.fn.invoke = (method, ...args) -> + for el, idx of this + jQuery(el)[method] ...args + + +exports import require 'kraken/util/event' + +backbone = exports.backbone = require 'kraken/util/backbone' +parser = exports.parser = require 'kraken/util/parser' +Cascade = exports.Cascade = require 'kraken/util/cascade' + +# HashSet = exports.HashSet = require 'kraken/util/hashset' +# BitString = exports.BitString = require 'kraken/util/bitstring' +# {crc32} = exports.{crc32} = require 'kraken/util/crc' + diff --git a/src/util/op.co b/src/util/op.co new file mode 100644 index 0000000..e434d65 --- /dev/null +++ b/src/util/op.co @@ -0,0 +1,154 @@ +DASH_PATTERN = /-/g + +STRIP_PAT = /(^\s*|\s*$)/g +strip = (s) -> + if s then s.replace STRIP_PAT, '' else s + +FALSEY = /^\s*(?:no|off|false)\s*$/i +parseBool = (s) -> + i = parseInt(s or 0) + !! if isNaN(i) then not FALSEY.test(s) else i + + + +module.exports = op = + I : (x) -> x + K : (k) -> -> k + nop : -> + kThis : -> this + kObject : -> {} + kArray : -> [] + + ### values + val : (def,o) -> o ? def + ok : (o) -> o? + notOk : (o) -> o!? + + ### manipulate function args/arity + first : (a) -> a + second : (_,a) -> a + nth : (n) -> + switch n + case 0 then op.first + case 1 then op.second + default -> arguments[n] + + # reverse the order of the first two args + flip : (fn) -> + (a, b) -> + arguments[0] = b + arguments[1] = a + fn.apply this, arguments + + # only pass n args + aritize : (fn, cxt, n) -> + [n, cxt] = [cxt, null] if arguments.length < 3 + -> fn.apply cxt ? this, [].slice.call(arguments, 0, n) + + # only pass the first argument + it : (fn, cxt) -> + -> fn.call cxt ? this, it + + + + ### reduce-ordered values & accessors + khas : (k,o) -> k in o + kget : (k,o) -> o[k] + defkget : (def,k,o) -> if k in o then o[k] else def + thisget : (k) -> this[k] + vkset : (o,v,k) -> o[k] = v if o and k?; o + + ### curry-ordered values & accessors + has : (o,k) -> k in o + get : (o,k) -> o[k] + getdef : (o,k,def) -> if k in o then o[k] else def + kvset : (o,k,v) -> o[k] = v if o and k?; o + thiskvset : (k,v) -> @[k] = v if k?; this + + prop : (k) -> (o) -> o[k] + method : (name, ...args) -> + (obj, ..._args) -> + obj[name] ...args.concat(_args) if obj?[name] + isK : (k) -> (v) -> v is k + + ### type coercion (w/ limited parameters for mapping) + parseBool : parseBool + toBool : parseBool + toInt : (v) -> parseInt v + toFloat : (v) -> parseFloat v + toStr : (v) -> String v + toRegExp : (v) -> new RegExp v + + toObject : (v) -> + if typeof v is 'string' and strip(v) + JSON.parse v + else + v + + toDate : (v) -> + return v if v!? or v instanceof Date + return new Date v if typeof v is 'number' + return new Date String(v).replace DASH_PATTERN, '/' + + + ### comparison + cmp : (x,y) -> if x < y then -1 else (if x > y then 1 else 0) + eq : (x,y) -> x == y + ne : (x,y) -> x != y + gt : (x,y) -> x > y + ge : (x,y) -> x >= y + lt : (x,y) -> x < y + le : (x,y) -> x <= y + + ### math + add : (x,y) -> x + y + sub : (x,y) -> x - y + mul : (x,y) -> x * y + div : (x,y) -> x / y + flrdiv : (x,y) -> Math.floor(x / y) + mod : (x,y) -> x % y + neg : (x) -> -x + log2 : (n) -> Math.log n / Math.LN2 + + + ### logic + is : (x,y) -> x is y + isnt : (x,y) -> x is not y + and : (x,y) -> x and y + or : (x,y) -> x or y + not : (x) -> not x + + ### bitwise + bitnot : (x) -> ~x + bitand : (x,y) -> x & y + bitor : (x,y) -> x | y + bitxor : (x,y) -> x ^ y + lshift : (x,y) -> x << y + rshift : (x,y) -> x >> y + # zrshift : (x,y) -> x >>> y + + ### binary + + # Binary representation of the number. + bin : (n) -> + do + s = (if n % 2 then '1' else '0') + (s or '') + n >>= 1 + while n + s + + # Number of bits needed to represent the absolute value of n. + binlen : (n) -> + bin Math.abs n .length + + # Returns a run of 1s of size n. + mask : (n) -> + (1 << n) - 1 + + # strings + chr : -> String.fromCharCode it + ord : -> String(it).charCodeAt 0 + encode : -> it and $ "
    #it
    " .html().replace /"/g, '"' + decode : -> it and $ "
    #it
    " .text() + strip : strip + diff --git a/src/util/parser.co b/src/util/parser.co new file mode 100644 index 0000000..1783703 --- /dev/null +++ b/src/util/parser.co @@ -0,0 +1,127 @@ +_ = require 'kraken/util/underscore' +op = require 'kraken/util/op' +{ BaseModel, BaseList, BaseView, Mixin, +} = require 'kraken/base' + + +/** + * @namespace Parsers by type. + */ +Parsers = exports.Parsers = + + parseBoolean: (v) -> + op.toBool v + + parseInteger: (v) -> + r = op.toInt v + unless isNaN r then r else null + + parseFloat: (v) -> + r = op.toFloat v + unless isNaN r then r else null + + parseString: (v) -> + if v? then op.toStr v else null + + parseDate: (v) -> + if v then op.toDate v else null + + parseRegExp: (v) -> + if v then op.toRegExp v else null + + parseArray: (v) -> + if v then op.toObject v else null + + parseObject: (v) -> + if v then op.toObject v else null + + parseFunction: (v) -> + if v and _.startswith String(v), 'function' + try eval "(#v)" catch err then null + else + null + + +# Aliases +Parsers.parseNumber = Parsers.parseFloat + + +/** + * @class Methods for a class to select parsers by type reflection. + * @mixin + */ +class exports.ParserMixin extends Mixin + this:: import Parsers + + (target) -> + return Mixin.call ParserMixin, target + + + # XXX: So I'm meh about mixing in the Parsers dictionary. + # + # - Pros: mixing in `parseXXX()` methods makes it easy to + # override in the target class. + # - Cons: `parse()` is a Backbone method, which bit me once + # already (hence `parseValue()`), so conflicts aren't unlikely. + # + # Other ideas: + # - Parsers live at `@__parsers__`, and each instance gets its own clone + # -> Parser lookup uses a Cascade from the object. (Why not just use prototype, tho?) + + parseValue: (v, type) -> + @getParser(type)(v) + + getParser: (type='String') -> + # If this is a known type and we have a parser for it, return that + fn = @["parse#type"] + return fn if typeof fn is 'function' + + # Handle compound/optional types + # XXX: handle 'or' by returning an array of parsers? + type = _ String(type).toLowerCase() + for t of <[ Integer Float Number Boolean Object Array Function ]> + if type.startsWith t.toLowerCase() + return @["parse#t"] + @defaultParser or @parseString + + getParserFromExample: (v) -> + return null unless v? + type = typeof v + + if type is not 'object' + @getParser type + else if _.isArray v + @getParser 'Array' + else + @getParser 'Object' + + + + +/** + * @class Basic model which mixes in the ParserMixin. + * @extends BaseModel + * @borrows ParserMixin + */ +ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin.mix do + constructor: function ParsingModel then BaseModel ... + + +/** + * @class Basic collection which mixes in the ParserMixin. + * @extends BaseList + * @borrows ParserMixin + */ +ParsingList = exports.ParsingList = BaseList.extend ParserMixin.mix do + constructor: function ParsingList then BaseList ... + + +/** + * @class Basic view which mixes in the ParserMixin. + * @extends BaseView + * @borrows ParserMixin + */ +ParsingView = exports.ParsingView = BaseView.extend ParserMixin.mix do + constructor: function ParsingView then BaseView ... + + diff --git a/src/util/timeseries/csv.co b/src/util/timeseries/csv.co new file mode 100644 index 0000000..e89053b --- /dev/null +++ b/src/util/timeseries/csv.co @@ -0,0 +1,115 @@ +_ = require 'kraken/util/underscore' +TimeSeriesData = require 'kraken/util/timeseries/timeseries' + + +DASH_PATTERN = /-/g +BLANK_LINE_PATTERN = /^(\s*)$/ +COMMENT_PATTERN = /\s*(#|\/\/).*$/ + +class CSVData extends TimeSeriesData + DEFAULT_OPTIONS : + colSep : ',' + rowSep : '\n' + defaultType : 'float' + customBars : false + customSep : ';' + errorBars : false + fractions : false + fractionSep : '/' + skipBlankLines : true + blankLinePat : BLANK_LINE_PATTERN + removeCommentedText : true + commentPat : COMMENT_PATTERN + replaceMissing : false + replaceMissingValue : 0 + replaceNaN : false + replaceNaNValue : 0 + padRows : false + padRowsValue : 0 + + + (data, opts) -> + super ... + + + /* * * * CSV Parsing * * * */ + + parseNumber: (s) -> + parseFloat s + + parseHiLo: (s) -> + s.split @options.customBars .map @parseNumber, this + + parseFraction: (s) -> + s.split @options.fractionSep .map @parseNumber, this + + parseDate: (s) -> + new Date s.replace DASH_PATTERN, '/' + + + /** + * Parses and imports a CSV string. + * + * @private + * @returns {this} + */ + parseData: (@rawData) -> + return this if typeof rawData is not 'string' + o = @options + + lines = rawData.split o.rowSep + return [] unless lines.length + first = lines[0] + + # Use the default delimiter or fall back to a tab if that makes sense. + delim = o.colSep + if first.indexOf(delim) is -1 and first.indexOf('\t') >= 0 + delim = '\t' + + rows = @rows = [] + @columns = [] + + parser = @parseNumber + parser = @parseHiLo if o.customBars + parser = @parseFraction if o.fractions + + hasHeaders = @labels.length is not 0 + for line, i of lines + line .= replace o.commentPat, '' if o.removeCommentedText + continue if o.skipBlankLines and (line.length is 0 or o.blankLinePat.test line) + + cols = line.split delim + unless hasHeaders + hasHeaders = true + @labels = cols.map -> _.strip it + continue + + continue unless cols.length > 1 + date = @parseDate cols.shift() + fields = cols.map parser, this + if o.errorBars + fields = fields.reduce do + (acc, v) -> + last = acc[acc.length-1] + unless last and last.length < 2 + acc.push last = [] + last.push v + acc + [] + + fields.unshift date + rows.push fields + fields.forEach (v, idx) ~> + @columns.push [] unless @columns[idx] + @columns[idx].push v + + @untransformedRows = _.merge [], @rows + this + + + + + +module.exports = exports = CSVData + + diff --git a/src/util/timeseries/index.co b/src/util/timeseries/index.co new file mode 100644 index 0000000..b583a6b --- /dev/null +++ b/src/util/timeseries/index.co @@ -0,0 +1,2 @@ +exports.TimeSeriesData = require 'kraken/util/timeseries/timeseries' +exports.CSVData = require 'kraken/util/timeseries/csv' diff --git a/src/util/timeseries/timeseries.co b/src/util/timeseries/timeseries.co new file mode 100644 index 0000000..8fad863 --- /dev/null +++ b/src/util/timeseries/timeseries.co @@ -0,0 +1,191 @@ +_ = require 'kraken/util/underscore' + + + +/** + * @class Represents a collection of data columns aligned along a common timeline. + */ +class TimeSeriesData + DEFAULT_OPTIONS : {} + + options : {} + labels : [] + types : [] + + untransformedRows : null # row-oriented (untransformed) + rows : null # row-oriented + columns : null # column-oriented (includes date column) + dateColumn : null # only date column + dataColumns : null # column-oriented (excludes date column) + + + /** + * @constructor + */ + (data, opts) -> + unless typeof data is 'string' or _.isArray data + [opts, data] = [data, null] + @options = _.clone(@DEFAULT_OPTIONS) import (opts or {}) + + @transforms = [] + @labels = @options.labels or [] + @types = @options.types or [] + + @parseData that if data or @options.data + @rebuildDerived() + + + /* * * * TimeSeriesData interface * * * */ + + + /** + * @returns {Array} List of rows, each of which includes all columns. + */ + getData: -> + @data + + /** + * @returns {Array} List of all columns (including date column). + */ + getColumns: -> + @columns + + /** + * @returns {Array} The date column. + */ + getDateColumn: -> + @dateColumn + + /** + * @returns {Array} List of all columns except the date column. + */ + getDataColumns: -> + @dataColumns + + /** + * @returns {Array} List of column labels. + */ + getLabels: -> + @labels + + + /* * * * Parsing * * * */ + + /** + * Subclass and override to perform preprocessing of the data. + * @private + */ + parseData : (rawData) -> + this + + /** + * Rebuilds the row-oriented data matrix from the columns. + * @private + */ + rebuildData: -> + @rows = _.zip ...@columns + @rebuildDerived() + + /** + * Rebuilds the column-oriented data matrix from the columns. + * @private + */ + rebuildColumns: -> + @columns = _.zip ...@rows + @rebuildDerived() + + /** + * @private + */ + rebuildDerived: -> + while @transforms.length < @columns.length + @transforms.push [] + @dateColumn = @columns[0] + @dataColumns = @columns.slice(1) + this + + + + /* * * * Data Transformation * * * */ + + /** + * Applies the stack of transforms to the data. + * + * TODO: Apply transforms in @getData()? + * @private + * @returns {this} + */ + applyTransforms: -> + for fns, idx of @transforms + for [fn, ctx] of fns + @columns[idx] .= map fn, ctx + @rebuildData() + + /** + * Clears all transforms and restores the original data. + * @returns {this} + */ + clearTransforms: -> + @transforms = [] + @rows = _.merge [], @untransformedRows + @rebuildColumns() + + /** + * Add a data transform to the specified columns. The function is + * applied one-by-one (in column-major order), replacing the data + * with the mapped result. + * + * @param {Number|Array} indices List one or more column indices to map. Negative + * numbers are offset from the end of the columns list. + * @param {Function} fn Mapping function of the form: + * `(single_value, row_idx, column) -> new_value` + * @param {Object} [ctx=this] Execution context for the function. + * @returns {this} + */ + addTransform: (indices, fn, ctx=this) -> + num_cols = @columns.length + if typeof idx is 'function' + [ctx, fn, indices] = [fn, indices, null] + unless indices? + indices = _.range num_cols + unless _.isArray indices + indices = [indices] + for idx of indices + idx %= num_cols + idx += num_cols if idx < 0 + @transforms[idx].push [fn, ctx] + @applyTransforms() + + /** + * Add a data transform to all columns except the date column. The function + * is applied one-by-one (in column-major order), replacing the data + * with the mapped result. + * + * @param {Function} fn Mapping function of the form: + * `(single_value, row_idx, column) -> new_value` + * @param {Object} [ctx=this] Execution context for the function. + * @returns {this} + */ + addDataTransform: (fn, ctx=this) -> + @addTransform _.range(1, @columns.length), fn, ctx + + + + /* * * * Misc * * * */ + + /** + * @returns {Array} Deep copy of the data rows (including all columns). + */ + toJSON: -> + _.merge [], @getData() + + toString: -> + labels = @labels + .map -> "'#it'" + .join ', ' + "#{@..name or @..displayName}(#labels)" + + + +module.exports = exports = TimeSeriesData + diff --git a/src/util/underscore/_functions.co b/src/util/underscore/_functions.co new file mode 100644 index 0000000..7327961 --- /dev/null +++ b/src/util/underscore/_functions.co @@ -0,0 +1,220 @@ +_ = require 'underscore' + + +slice = [].slice +hasOwn = {}.hasOwnProperty +objToString = {}.toString + +toArray = _.toArray + + + +decorate = (fn) -> + if not fn.__decorated__ + for name of _pet.FUNCTION_METHODS + m = _[name] + fn[name] = m.__methodized__ or methodize m + fn.__decorated__ = true + return fn + +methodize = (fn) -> + m = fn.__methodized__ + return m if m + + g = fn.__genericized__ + return g.__wraps__ if g and g.__wraps__ + + m = fn.__methodized__ = (args...) -> + args.unshift this + return fn.apply this, args + + m.__wraps__ = fn + return decorate m + + + +_pet = module.exports = \ + function pet (o, start=0, end=undefined) -> + if _.isArguments o + o = _.toArray o, start, end + + return decorate o if typeof o is 'function' + return _ o + +# function methods to be attached on call to _(fn) +_pet.FUNCTION_METHODS = [ + 'bind', 'bindAll', 'memoize', + 'delay', 'defer', 'throttle', 'debounce', 'once', 'after', + 'wrap', 'compose', + 'unwrap', 'partial', 'curry', 'flip', 'methodize', 'aritize', 'limit' +] + + +class2name = "Boolean Number String Function Array Date RegExp Object" + .split(" ") + .reduce ((class2name, name) -> + class2name[ "[object "+name+"]" ] = name + return class2name), {} + + +## Objects +_.mixin + + has: (o, v) -> + vals = if _.isArray(o) then o else _.values(o) + return vals.indexOf(v) is not -1 + + remove: (o, vs...) -> + if _.isArray(o) + _.each vs, (v) -> + idx = o.indexOf v + if idx is not -1 + o.splice idx, 1 + else + _.each o, (v, k) -> + if vs.indexOf(v) != -1 + delete o[k] + return o + + set: (o, key, value, def) -> + if o and key? and (value? or def?) + o[key] = value ? def + return o + + attr: (o, key, value, def) -> + return o if not o or key is undefined + + if _.isPlainObject key + return _.extend o, key + + if (value ? def) is not undefined + return _.set o, key, value, def + + return o[key] + + + +## Types +_.mixin + + basicTypeName: (o) -> + return if o is null then "null" else (class2name[objToString.call(o)] || "Object") + + isWindow: (o) -> + return o and typeof o is "object" and "setInterval" of o + + isPlainObject: (o) -> + # Must be an Object. + # Because of IE, we also have to check the presence of the constructor property. + # Make sure that DOM nodes and window objects don't pass through, as well + if not o or basicTypeName(o) is not "Object" or o.nodeType or _.isWindow(o) + return false + + # Not own constructor property? must be Object + C = o.constructor + if C and not hasOwn.call(o, "constructor") and not hasOwn.call(C.prototype, "isPrototypeOf") + return false + + # Own properties are enumerated firstly, so to speed up, + # if last one is own, then all properties are own. + for key in o + ; # semicolon **on new line** is required by coffeescript to denote empty statement. + + return key is void or hasOwn.call(o, key) + + +## Arrays +_.mixin + + toArray: (iterable, start=0, end=undefined) -> + _.slice toArray(iterable), start, end + + flatten: (A) -> + _.reduce do + slice.call(arguments) + (flat, v) -> + flat.concat( if _.isArray v then _.reduce(v, arguments.callee, []) else v ) + [] + + + +## Functions +_ofArity = _.memoize do + (n, limit) -> + args = ( '$'+i for i from 0 til n ).join(',') + name = ( if limit then 'limited' else 'artized' ) + apply_with = ( if limit then "[].slice.call(arguments, 0, #{n})" else 'arguments' ) + return eval " + (function #{name}(fn){ + var _fn = function(#{args}){ return fn.apply(this, #{apply_with}); }; + _fn.__wraps__ = fn; + return _(_fn); + })" + +_.mixin do + methodize + + unwrap: (fn) -> + (fn and _.isFunction(fn) and _.unwrap(fn.__wraps__)) or fn + + + partial: (fn, ...args) -> + partially = -> + fn.apply this, args.concat slice.call(arguments) + _ partially import { __wraps__:fn } + + + genericize: (fn) -> + return that if fn.__genericized__ + return that if fn.__methodized__?.__wraps__ + + fn.__genericized__ = (...args) -> + fn.apply args.shift(), args + + _ fn.__genericized__ import { __wraps__:fn } + + + curry: (fn, ...args) -> + return fn unless _.isFunction fn + return fn.apply this, args if fn.__curried__ + + L = fn.length or _.unwrap(fn).length + return fn.apply this, args if args.length >= L + + curried = -> + _args = args.concat slice.call(arguments) + return fn.apply this, _args if _args.length >= L + _args.unshift fn + _.curry.apply this, _args + + _ curried import + __wraps__ : fn + __curried__ : args + + + flip: (fn) -> + return that if fn.__flipped__ + + fn.__flipped__ = -> + [arguments[0], arguments[1]] = [arguments[1], arguments[0]] + fn ... + + _ fn.__flipped__ import { __wraps__:fn } + + + aritize: (fn, n) -> + return fn if fn.length is n + fn.__aritized__ or= {} + fn.__aritized__[n] or= _ofArity(n, false)(fn) + + + limit: (fn, n) -> + fn.__limited__ or= {} + fn.__limited__[n] or= _ofArity(n, true)(fn) + + + + + + +_.extend _pet, _ diff --git a/src/util/underscore/array.co b/src/util/underscore/array.co new file mode 100644 index 0000000..76e078a --- /dev/null +++ b/src/util/underscore/array.co @@ -0,0 +1,48 @@ +_ = require 'underscore' + +I = -> it +defined = (o) -> o? + +_array = do + /** + * Transforms an Array of tuples (two-element Arrays) into an Object, such that for each + * tuple [k, v]: + * result[k] = v if filter(v) + * @param {Array} o A collection. + * @param {Function} [filter=defined] Optional filter function. If omitted, will + * exclude `undefined` and `null` values. + * @return {Object} Transformed result. + */ + generate : (o, filter=defined) -> + _.reduce do + o + (acc, [k, v], idx) -> + if k and (not filter or filter(v, k)) + acc[k] = v + acc + {} + + /** + * As {@link _.generate}, but first transforms the collection using `fn`. + * @param {Array} o A collection. + * @param {Function} [fn=I] Transformation function. Defaults to the identity transform. + * @param {Function} [filter=defined] Optional filter function. If omitted, will + * exclude `undefined` and `null` values. + * @param {Object} [context=o] Function context. + * @return {Object} Transformed result. + */ + synthesize : (o, fn=I, filter=defined, context) -> + _array.generate _.map(o, fn, context), filter + + + /** + * Symmetric Difference + */ + xor : (a, b) -> + a = _.values a + b = _.values b + return _.union _.difference(a,b), _.difference(b,a) + + + +exports import _array diff --git a/src/util/underscore/class.co b/src/util/underscore/class.co new file mode 100644 index 0000000..1381cb4 --- /dev/null +++ b/src/util/underscore/class.co @@ -0,0 +1,53 @@ +_ = require 'underscore' + + +_cls = + + /** + * @returns {Array} The list of all superclasses for this class + * or object. Typically does not include Object or Function due to + * the prototype's constructor being set by the subclass. + */ + getSuperClasses : function getSuperClasses(Cls) + return [] unless Cls + + if Cls.__superclass__ or Cls.superclass or Cls.__super__?.constructor + superclass = that unless that is Cls + unless superclass + Cls = Cls.constructor if typeof Cls is not 'function' + if Cls.__superclass__ or Cls.superclass or Cls.__super__?.constructor + superclass = that unless that is Cls + unless superclass then [] + else [superclass].concat getSuperClasses superclass + + /** + * Looks up an attribute on the prototype of each class in the class + * hierarchy. Values from Object or Function are not typically included -- + * see the note at `getSuperClasses()`. + * + * @param {Object} obj Object on which to reflect. + * @param {String} prop Property to nab. + * @returns {Array} List of the values, from closest parent to furthest. + */ + pluckSuper : (obj, prop) -> + return [] unless obj + _ _cls.getSuperClasses(obj) .chain() + .pluck 'prototype' + .pluck prop + .value() + + /** + * As `.pluckSuper()` but includes value of `prop` on passed `obj`. Values + * from Object or Function are not typically included -- see the note + * at `getSuperClasses()`. + * + * @returns {Array} List of the values, starting with the object's own + * value, and then moving from closest parent to furthest. + */ + pluckSuperAndSelf : (obj, prop) -> + return [] unless obj + [ obj[prop] ].concat _cls.pluckSuper(obj, prop) + + + +exports import _cls diff --git a/src/util/underscore/function.co b/src/util/underscore/function.co new file mode 100644 index 0000000..a6e50b7 --- /dev/null +++ b/src/util/underscore/function.co @@ -0,0 +1,27 @@ +_ = require 'underscore' + +_fn = + + /** + * Decorates a function so that its receiver (`this`) is always added as the + * first argument, followed by the call arguments. + * @returns {Function} + */ + methodize : (fn) -> + m = fn.__methodized__ + return m if m + + g = fn.__genericized__ + return that if g?.__wraps__ + + m = fn.__methodized__ = (...args) -> + args.unshift this + fn.apply this, args + + m.__wraps__ = fn + m + + + +exports import _fn + diff --git a/src/util/underscore/index.co b/src/util/underscore/index.co new file mode 100644 index 0000000..f59c17f --- /dev/null +++ b/src/util/underscore/index.co @@ -0,0 +1,29 @@ +_ = require 'underscore' +_.str = require 'underscore.string' +_.mixin _.str.exports() + +_.mixin require 'kraken/util/underscore/function' +_.mixin require 'kraken/util/underscore/array' +_.mixin require 'kraken/util/underscore/object' +_.mixin require 'kraken/util/underscore/class' +_.mixin require 'kraken/util/underscore/kv' +_.mixin require 'kraken/util/underscore/string' + + +## Debug +_.dump = (o, label='dump', expanded=true) -> + if not _.isArray(o) and _.isObject(o) + if expanded + console.group label + else + console.groupCollapsed label + for k, v in o + console.log "#k:", v + console.groupEnd() + else + console.log label, o + o + + +module.exports = exports = _ + diff --git a/src/util/underscore/kv.co b/src/util/underscore/kv.co new file mode 100644 index 0000000..4eb5389 --- /dev/null +++ b/src/util/underscore/kv.co @@ -0,0 +1,60 @@ +_ = require 'underscore' + + +_kv = do + + /** + * Transforms an object to a string of URL-encoded KV-pairs (aka "www-form-encoding"). + */ + toKV: (o, item_delim='&', kv_delim='=') -> + _.reduce do + o + (acc, v, k) -> + acc.push encodeURIComponent(k)+kv_delim+encodeURIComponent(v) if k + acc + [] + .join item_delim + + /** + * Restores an object from a string of URL-encoded KV-pairs (aka "www-form-encoding"). + */ + fromKV: (qs, item_delim='&', kv_delim='=') -> + _.reduce do + qs.split item_delim + (acc, pair) -> + idx = pair.indexOf kv_delim + if idx is not -1 + [k, v] = [pair.slice(0, idx), pair.slice(idx+1)] + else + [k, v] = [pair, ''] + acc[ decodeURIComponent k ] = decodeURIComponent v if k + acc + {} + + /** + * Copies and flattens a tree of sub-objects into namespaced keys on the parent object, such + * that `{ "foo":{ "bar":1 } }` becomes `{ "foo.bar":1 }`. + */ + collapseObject: (obj, parent={}, prefix='') -> + prefix += '.' if prefix + _.each obj, (v, k) -> + if _.isPlainObject v + _.collapseObject v, parent, prefix+k + else + parent[prefix+k] = v + parent + + /** + * Inverse of `.collapseObject()` -- copies and expands any dot-namespaced keys in the object, such + * that `{ "foo.bar":1 }` becomes `{ "foo":{ "bar":1 }}`. + */ + uncollapseObject: (obj) -> + _.reduce do + obj + (acc, v, k) -> + _.setNested acc, k, v, {+ensure} + acc + {} + + +exports import _kv diff --git a/src/util/underscore/object.co b/src/util/underscore/object.co new file mode 100644 index 0000000..9bc6e23 --- /dev/null +++ b/src/util/underscore/object.co @@ -0,0 +1,270 @@ +_ = require 'underscore' + +getProto = Object.getPrototypeOf +OBJ_PROTO = Object.prototype +{ + hasOwnProperty : hasOwn + toString : objToString +} = {} + +/** + * Default options for delegate-accessor functions. + */ +DEFAULT_DELEGATE_OPTIONS = exports.DEFAULT_DELEGATE_OPTIONS = + getter : 'get' + setter : 'set' + deleter : 'unset' + +/** + * Tombstone for deleted, non-passthrough keys. + */ +TOMBSTONE = exports.TOMBSTONE = {} + +/** + * Default options for nested-accessor functions. + */ +DEFAULT_NESTED_OPTIONS = exports.DEFAULT_NESTED_OPTIONS = + {-ensure, tombstone:TOMBSTONE} import DEFAULT_DELEGATE_OPTIONS + + + +/** + * @namespace Functions for working with objects and object graphs. + */ +_obj = do + + + # isPlainObject : (o) -> + # !!( o and _.isObject(o) and OBJ_PROTO is getProto(o) ) + + /** + * @returns {Boolean} Whether value is a plain object or not. + */ + isPlainObject: (obj) -> + # Must be an Object. + # Because of IE, we also have to check the presence of the constructor property. + # Make sure that DOM nodes and window objects don't pass through, as well. + if not obj or objToString.call(obj) !== "[object Object]" or obj.nodeType or obj.setInterval + return false + + # Not own constructor property must be Object + return false if obj.constructor + and not hasOwn.call(obj, "constructor") + and not hasOwn.call(obj.constructor.prototype, "isPrototypeOf") + + # Own properties are enumerated firstly, so to speed up, + # if last one is own, then all properties are own. + for key in obj then ; + return key is void or hasOwn.call obj, key + + + /** + * In-place removal of a value from an Array or Object. + */ + remove: (obj, v) -> + values = [].slice.call arguments, 1 + if _.isArray(obj) or obj instanceof Array + for v of values + idx = obj.indexOf v + obj.splice idx, 1 if idx is not -1 + else + for k, v in obj + delete obj[k] if -1 is not values.indexOf v + obj + + + /** + * Converts the collection to a list of its items: + * - Objects become a list of `[key, value]` pairs. + * - Strings become a list of characters. + * - Arguments objects become an array. + * - Arrays are copied. + */ + items: (obj) -> + if _.isObject(obj) and not _.isArguments(obj) + _.map obj, (v, k) -> [k, v] + else + [].slice.call obj + + + + ### Delegating Accessors + + isMember: (obj, v) -> + values = _.unique [].slice.call arguments, 1 + common = _.intersection _.values(obj), values + _.isEqual values, common + + get: (obj, key, def, opts) -> + return unless obj? + getter = opts?.getter or 'get' + if typeof obj[getter] is 'function' + obj[getter] key, def, opts + else + if obj[key] is not void then obj[key] else def + + set: (obj, key, value, opts) -> + return unless obj? + if key? and _.isObject(key) + [values, opts] = [key, value] + else + values = { "#key": value } + + setter = opts?.setter or 'set' + if typeof obj[setter] is 'function' + for key, value in values + obj[setter] key, value, opts + else + for key, value in values + obj[key] = value + + obj + + unset: (obj, key, opts) -> + return unless obj? + deleter = opts?.deleter or 'unset' + if typeof obj[deleter] is 'function' + obj[deleter] key, opts + else + delete obj[key] + + + + + + ### Nested Acccessors + + /** + * Searches a heirarchical object for a given subkey specified in dotted-property syntax, + * respecting sub-object accessor-methods (e.g., 'get', 'set') if they exist. + * + * @param {Object} base The object to serve as the root of the property-chain. + * @param {Array|String} chain The property-chain to lookup. + * @param {Object} [opts] Options: + * @param {Boolean} [opts.ensure=false] If true, intermediate keys that are `null` or + * `undefined` will be filled in with a new empty object `{}`, ensuring the get will + * return valid metadata. + * @param {String} [opts.getter="get"] Name of the sub-object getter method use if it exists. + * @param {String} [opts.setter="set"] Name of the sub-object setter method use if it exists. + * @param {String} [opts.deleter="unset"] Name of the sub-object deleter method use if it exists. + * @param {Object} [opts.tombstone=TOMBSTONE] Sentinel value to be interpreted as no-passthrough, + * forcing the lookup to fail and return `undefined`. TODO: opts.returnTombstone + * @returns {undefined|Object} If found, the object is of the form + * `{ key: Qualified key name, obj: Parent object of key, val: Value at obj[key], opts: Options }`. + * Otherwise `undefined`. + */ + getNestedMeta : (obj, chain, opts) -> + chain = chain.split('.') if typeof chain is 'string' + len = chain.length - 1 + opts = _.clone(DEFAULT_NESTED_OPTIONS) import (opts or {}) + + _.reduce do + chain + (obj, key, idx) -> + return unless obj? + val = _.get obj, key, void, opts + + if val is opts.tombstone + return unless ops.ensure + val = void + + if idx is len + return { key, val, obj, opts } + if not val? and opts.ensure + val = {} + _.set obj, key, val, opts + val + obj + + /** + * Searches a heirarchical object for a given subkey specified in dotted-property syntax. + * @param {Object} obj The object to serve as the root of the property-chain. + * @param {Array|String} chain The property-chain to lookup. + * @param {Any} [def=undefined] Value to return if lookup fails. + * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`. + * @returns {null|Object} If found, returns the value, and otherwise `default`. + */ + getNested : (obj, chain, def, opts) -> + meta = _.getNestedMeta obj, chain, opts + return def if meta?.val is void + meta.val + + /** + * Searches a heirarchical object for a given subkey specified in + * dotted-property syntax, setting it with the provided value if found. + * @param {Object} obj The object to serve as the root of the property-chain. + * @param {Array|String} chain The property-chain to lookup. + * @param {Any} value The value to set. + * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`. + * @returns {undefined|Any} If found, returns the old value, and otherwise `undefined`. + */ + setNested : (obj, chain, value, opts) -> + return unless meta = _.getNestedMeta obj, chain, opts + {obj, key, val, opts} = meta + _.set obj, key, value, opts + val + + /** + * Searches a heirarchical object for a potentially-nested key and removes it. + * + * @param {Object} obj The root of the lookup chain. + * @param {String|Array} chain The chain of property-keys to navigate. + * Nested keys can be supplied as a dot-delimited string (e.g., `_.unsetNested(obj, 'user.name')`), + * or an array of strings, allowing for keys with dots (eg., + * `_.unsetNested(obj, ['products', 'by_price', '0.99'])`). + * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`. + * @returns {undefined|Any} The old value if found; otherwise `undefined`. + */ + unsetNested : (obj, chain, opts) -> + return unless meta = _.getNestedMeta obj, chain, opts + {obj, key, val, opts} = meta + _.unset obj, key, opts + val + + + /** + * Recursively merges together any number of donor objects into the target object. + * Modified from `jQuery.extend()`. + * + * @param {Object} target Target object of the merge. + * @param {Object} ...donors Donor objects. + * @returns {Object} + */ + merge: (target={}, ...donors) -> + # Handle case when target is a string or something (possible in deep copy) + unless typeof target is "object" or _.isFunction(target) + target = if _.isArray donors[0] then [] else {} + + for donor of donors + # Only deal with non-null/undefined values + continue unless donor? + + # Extend the base object + _.each donor, (value, key) -> + current = target[key] + + # Prevent never-ending loop + return if target is value + + # Recurse if we're merging plain objects or arrays + if value and (_.isPlainObject(value) or (valueIsArray = _.isArray(value))) + if valueIsArray + current = [] unless _.isArray current + else + current = {} unless current and typeof current is 'object' + + # Never move original objects, clone them + _.set target, key, _.merge(current, value) + + # Don't bring in undefined values + else if value is not void + _.set target, key, value + + # Return the modified object + target + + + + + +exports import _obj diff --git a/src/util/underscore/string.co b/src/util/underscore/string.co new file mode 100644 index 0000000..9b1ee6e --- /dev/null +++ b/src/util/underscore/string.co @@ -0,0 +1,75 @@ +_ = require 'underscore' +_str = require 'underscore.string' + +_string = do + + + + /** + * As _.str.chop but from the right. + */ + rchop : (s, step) -> + s = String s + i = s.length + step = Number step + out = [] + return [s] if step <= 0 + while i > 0 + out.unshift s.slice Math.max(0, i-step), i + i -= step + out + + + drop : (s, ...parts) -> + do + starting = s + for part of parts + s .= slice part.length if _str.startsWith s, part + s .= slice 0, s.length-part.length if _str.endsWith s, part + while s and s is not starting + s + + ldrop : (s, ...parts) -> + do + starting = s + for part of parts + s .= slice part.length if _str.startsWith s, part + while s and s is not starting + s + + rdrop : (s, ...parts) -> + do + starting = s + for part of parts + s .= slice 0, s.length-part.length if _str.endsWith s, part + while s and s is not starting + s + + # Converts to snake_case, concatenates the key-value pair (with '_'), normalizing _'s. + # If only a key is given, domize auto-curries and waits for a second argument. + domize : (key='', value='') -> + key = _str.trim _str.underscored(key), '_' + if arguments.length <= 1 + arguments.callee.bind this, key + else + "#{key}_#{_str.trim _str.underscored(value), '_'}" + + shortname: (s) -> + return s if s.length <= 6 + parts = _ s + .chain() + .underscored() + .trim '_' + .value() + .replace /_+/g, '_' + .split '_' + .map -> _.capitalize it.slice 0, 2 + return s if parts.length is 1 #and s.length <= 8 + parts.shift().toLowerCase() + parts.join('') + + +_string import do + dropLeft : _string.ldrop + dropRight : _string.rdrop + +exports import _string -- 1.7.0.4