From: David Schoonover Date: Tue, 10 Jul 2012 13:19:39 +0000 (-0700) Subject: Adds compiled JS source files to be published with the npm distribution. X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=3a33132e354923030515b2f9bb0fb94ed879fa2d;p=kraken-ui.git Adds compiled JS source files to be published with the npm distribution. --- diff --git a/lib/app.js b/lib/app.js new file mode 100644 index 0000000..b4937f3 --- /dev/null +++ b/lib/app.js @@ -0,0 +1,58 @@ +var Backbone, op, AppView, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Application view, automatically attaching to an existing element + * found at `appSelector`. + * @extends Backbone.View + */ +AppView = exports.AppView = Backbone.View.extend({ + appSelector: '#content .inner' + /** + * @constructor + */, + constructor: (function(){ + function AppView(options){ + var that, _this = this; + options == null && (options = {}); + if (typeof options === 'function') { + this.initialize = options; + options = {}; + } else { + if (that = options.initialize) { + this.initialize = that; + } + } + if (that = options.appSelector) { + this.appSelector = that; + } + options.el || (options.el = jQuery(this.appSelector)[0]); + Backbone.View.call(this, options); + jQuery(function(){ + return _this.render(); + }); + return this; + } + return AppView; + }()) + /** + * Override to set up your app. This method may be passed + * as an option to the constructor. + */, + initialize: function(){} + /** + * Append subviews. + */, + render: function(){ + var _ref; + if (this.view && !((_ref = this.view.$el.parent()) != null && _ref.length)) { + return this.$el.append(this.view.el); + } + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}); \ No newline at end of file diff --git a/lib/app.mod.js b/lib/app.mod.js new file mode 100644 index 0000000..e816fc5 --- /dev/null +++ b/lib/app.mod.js @@ -0,0 +1,62 @@ +require.define('/node_modules/kraken/app.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, AppView, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Application view, automatically attaching to an existing element + * found at `appSelector`. + * @extends Backbone.View + */ +AppView = exports.AppView = Backbone.View.extend({ + appSelector: '#content .inner' + /** + * @constructor + */, + constructor: (function(){ + function AppView(options){ + var that, _this = this; + options == null && (options = {}); + if (typeof options === 'function') { + this.initialize = options; + options = {}; + } else { + if (that = options.initialize) { + this.initialize = that; + } + } + if (that = options.appSelector) { + this.appSelector = that; + } + options.el || (options.el = jQuery(this.appSelector)[0]); + Backbone.View.call(this, options); + jQuery(function(){ + return _this.render(); + }); + return this; + } + return AppView; + }()) + /** + * Override to set up your app. This method may be passed + * as an option to the constructor. + */, + initialize: function(){} + /** + * Append subviews. + */, + render: function(){ + var _ref; + if (this.view && !((_ref = this.view.$el.parent()) != null && _ref.length)) { + return this.$el.append(this.view.el); + } + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}); + +}); diff --git a/lib/base/base-mixin.js b/lib/base/base-mixin.js new file mode 100644 index 0000000..eba7b88 --- /dev/null +++ b/lib/base/base-mixin.js @@ -0,0 +1,216 @@ +var Backbone, op, BaseBackboneMixin, Mixin, mixinBase, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseBackboneMixin = exports.BaseBackboneMixin = { + initialize: function(){ + return this.__apply_bind__(); + } + /** + * 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__: function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + } + /** + * 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: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (this[lock]) { + return this; + } + this[lock] = true; + this.trigger(event, this); + return this; + } + /** + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @returns {this} + */, + resetReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (!this[lock]) { + return this; + } + this[lock] = false; + this.trigger(event + "-reset", this); + return 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: function(events, callback, context){ + context == null && (context = this); + if (!callback) { + return this; + } + Backbone.Events.on.apply(this, arguments); + if (this.ready && _.contains(events.split(/\s+/), 'ready')) { + callback.call(context, this); + } + return this; + }, + makeHandlersForCallback: function(cb){ + var _this = this; + return { + success: function(){ + return cb.call(_this, [null].concat(arguments)); + }, + error: function(it){ + return cb.call(_this, it); + } + }; + } + /** + * Count of outstanding tasks. + * @type Number + */, + waitingOn: 0 + /** + * Increment the waiting task counter. + * @returns {this} + */, + wait: function(){ + var count; + count = this.waitingOn; + this.waitingOn += 1; + if (count === 0 && this.waitingOn > 0) { + this.trigger('start-waiting', this); + } + return this; + } + /** + * Decrement the waiting task counter. + * @returns {this} + */, + unwait: function(){ + var count; + count = this.waitingOn; + this.waitingOn -= 1; + if (this.waitingOn === 0 && count > 0) { + this.trigger('stop-waiting', this); + } + return 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: function(fn){ + var self; + self = this; + return function(){ + self.unwait(); + return fn.apply(this, arguments); + }; + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.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 + * + */ +exports.Mixin = Mixin = (function(){ + /** + * Mixes this mixin into the target. If `target` is not a class, a new + * object will be returned which inherits from the mixin. + */ + Mixin.displayName = 'Mixin'; + var prototype = Mixin.prototype, constructor = Mixin; + Mixin.mix = function(target){ + var MixinClass; + if (!target) { + return that; + } + MixinClass = Mixin; + if (this instanceof Mixin) { + MixinClass = this.constructor; + } + if (this instanceof Function) { + MixinClass = this; + } + if (typeof target === 'function') { + __import(target.prototype, MixinClass.prototype); + } else { + target = __import(_.clone(MixinClass.prototype), target); + } + (target.__mixins__ || (target.__mixins__ = [])).push(MixinClass); + return target; + }; + /** + * Coco metaprogramming hook to propagate class properties and methods. + */ + Mixin.extended = function(SubClass){ + var SuperClass, k, v, _own = {}.hasOwnProperty; + SuperClass = this; + for (k in SuperClass) if (_own.call(SuperClass, k)) { + v = SuperClass[k]; + if (!SubClass[k]) { + SubClass[k] = v; + } + } + return SubClass; + }; + function Mixin(){} + return Mixin; +}()); +/** + * Mixes BaseBackboneMixin into another object or prototype. + * @returns {Object} The merged prototype object. + */ +mixinBase = exports.mixinBase = function(){ + var bodies; + bodies = __slice.call(arguments); + return _.extend.apply(_, [_.clone(BaseBackboneMixin)].concat(__slice.call(bodies))); +}; +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base-mixin.mod.js b/lib/base/base-mixin.mod.js new file mode 100644 index 0000000..e2c68e2 --- /dev/null +++ b/lib/base/base-mixin.mod.js @@ -0,0 +1,220 @@ +require.define('/node_modules/kraken/base/base-mixin.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, Mixin, mixinBase, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseBackboneMixin = exports.BaseBackboneMixin = { + initialize: function(){ + return this.__apply_bind__(); + } + /** + * 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__: function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + } + /** + * 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: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (this[lock]) { + return this; + } + this[lock] = true; + this.trigger(event, this); + return this; + } + /** + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @returns {this} + */, + resetReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (!this[lock]) { + return this; + } + this[lock] = false; + this.trigger(event + "-reset", this); + return 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: function(events, callback, context){ + context == null && (context = this); + if (!callback) { + return this; + } + Backbone.Events.on.apply(this, arguments); + if (this.ready && _.contains(events.split(/\s+/), 'ready')) { + callback.call(context, this); + } + return this; + }, + makeHandlersForCallback: function(cb){ + var _this = this; + return { + success: function(){ + return cb.call(_this, [null].concat(arguments)); + }, + error: function(it){ + return cb.call(_this, it); + } + }; + } + /** + * Count of outstanding tasks. + * @type Number + */, + waitingOn: 0 + /** + * Increment the waiting task counter. + * @returns {this} + */, + wait: function(){ + var count; + count = this.waitingOn; + this.waitingOn += 1; + if (count === 0 && this.waitingOn > 0) { + this.trigger('start-waiting', this); + } + return this; + } + /** + * Decrement the waiting task counter. + * @returns {this} + */, + unwait: function(){ + var count; + count = this.waitingOn; + this.waitingOn -= 1; + if (this.waitingOn === 0 && count > 0) { + this.trigger('stop-waiting', this); + } + return 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: function(fn){ + var self; + self = this; + return function(){ + self.unwait(); + return fn.apply(this, arguments); + }; + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.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 + * + */ +exports.Mixin = Mixin = (function(){ + /** + * Mixes this mixin into the target. If `target` is not a class, a new + * object will be returned which inherits from the mixin. + */ + Mixin.displayName = 'Mixin'; + var prototype = Mixin.prototype, constructor = Mixin; + Mixin.mix = function(target){ + var MixinClass; + if (!target) { + return that; + } + MixinClass = Mixin; + if (this instanceof Mixin) { + MixinClass = this.constructor; + } + if (this instanceof Function) { + MixinClass = this; + } + if (typeof target === 'function') { + __import(target.prototype, MixinClass.prototype); + } else { + target = __import(_.clone(MixinClass.prototype), target); + } + (target.__mixins__ || (target.__mixins__ = [])).push(MixinClass); + return target; + }; + /** + * Coco metaprogramming hook to propagate class properties and methods. + */ + Mixin.extended = function(SubClass){ + var SuperClass, k, v, _own = {}.hasOwnProperty; + SuperClass = this; + for (k in SuperClass) if (_own.call(SuperClass, k)) { + v = SuperClass[k]; + if (!SubClass[k]) { + SubClass[k] = v; + } + } + return SubClass; + }; + function Mixin(){} + return Mixin; +}()); +/** + * Mixes BaseBackboneMixin into another object or prototype. + * @returns {Object} The merged prototype object. + */ +mixinBase = exports.mixinBase = function(){ + var bodies; + bodies = __slice.call(arguments); + return _.extend.apply(_, [_.clone(BaseBackboneMixin)].concat(__slice.call(bodies))); +}; +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/base-model.js b/lib/base/base-model.js new file mode 100644 index 0000000..7a4a6d6 --- /dev/null +++ b/lib/base/base-model.js @@ -0,0 +1,254 @@ +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ +BaseModel = exports.BaseModel = Backbone.Model.extend(mixinBase({ + constructor: (function(){ + function BaseModel(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Model.apply(this, arguments); + } + return BaseModel; + }()), + url: function(){ + return this.urlRoot + "/" + this.get('id') + ".json"; + }, + has: function(key){ + return this.get(key) != null; + }, + get: function(key){ + return _.getNested(this.attributes, key); + } + /** + * 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: function(){ + console.log(this + ".load()"); + this.loader({ + start: this.loadModel, + completeEvent: 'fetch-success' + }); + return 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: function(opts){ + var _this = this; + opts == null && (opts = {}); + opts = (__import({ + force: false, + readyIfError: false, + startEvent: 'load', + completeEvent: 'load-success', + errorEvent: 'load-error' + }, opts)); + if (opts.force) { + this.resetReady(); + } + if (!opts.start) { + throw new Error('You must specify a `start` function to start loading!'); + } + if (this.loading || this.ready) { + return this; + } + this.wait(); + this.loading = true; + this.trigger(opts.startEvent, this); + this.once(opts.completeEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.completeEvent !== 'load-success') { + _this.trigger('load-success', _this); + } + return _this.triggerReady(); + }); + this.once(opts.errorEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.errorEvent !== 'load-error') { + _this.trigger('load-error', _this); + } + if (opts.readyIfError) { + return _this.triggerReady(); + } + }); + opts.start.call(this); + return this; + } + /** + * Runs `.fetch()`, triggering a `fetch` event at start, and + * `fetch-success` / `fetch-error` on completion. + * + * @protected + * @returns {this} + */, + loadModel: function(){ + var _this = this; + this.wait(); + this.trigger('fetch', this); + this.fetch({ + success: function(){ + _this.unwait(); + return _this.trigger('fetch-success', _this); + }, + error: function(){ + _this.unwait(); + return _this.trigger.apply(_this, ['fetch-error', _this].concat(__slice.call(arguments))); + } + }); + return this; + }, + serialize: function(v){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return 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: function(opts){ + var kvo, k, v; + opts == null && (opts = {}); + opts = (__import({ + keepFunctions: false + }, opts)); + kvo = _.collapseObject(this.toJSON()); + for (k in kvo) { + v = kvo[k]; + if (opts.keepFunctions || typeof v !== 'function') { + kvo[k] = this.serialize(v); + } + } + return kvo; + } + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + } + /** + * @returns {String} URL identifying this model. + */, + toURL: function(){ + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "(cid=" + this.cid + ", id=" + this.id + ")"; + } +})); +__import(BaseModel, { + /** + * 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: function(o, item_delim, kv_delim){ + var Cls; + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + if (typeof o === 'string') { + o = _.fromKV(o, item_delim, kv_delim); + } + Cls = typeof this === 'function' + ? this + : this.constructor; + return 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({ + constructor: (function(){ + function BaseList(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Collection.apply(this, arguments); + } + return BaseList; + }()), + getIds: function(){ + return this.models.map(function(it){ + return it.id || it.get('id') || it.cid; + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.toJSON()); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "[" + this.length + "]"; + }, + toStringWithIds: function(){ + var modelIds; + modelIds = this.models.map(function(it){ + var _ref; + return "\"" + ((_ref = it.id) != null + ? _ref + : it.cid) + "\""; + }).join(', '); + return this.getClassName() + "[" + this.length + "](" + modelIds + ")"; + } +})); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base-model.mod.js b/lib/base/base-model.mod.js new file mode 100644 index 0000000..1fdd53b --- /dev/null +++ b/lib/base/base-model.mod.js @@ -0,0 +1,258 @@ +require.define('/node_modules/kraken/base/base-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ +BaseModel = exports.BaseModel = Backbone.Model.extend(mixinBase({ + constructor: (function(){ + function BaseModel(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Model.apply(this, arguments); + } + return BaseModel; + }()), + url: function(){ + return this.urlRoot + "/" + this.get('id') + ".json"; + }, + has: function(key){ + return this.get(key) != null; + }, + get: function(key){ + return _.getNested(this.attributes, key); + } + /** + * 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: function(){ + console.log(this + ".load()"); + this.loader({ + start: this.loadModel, + completeEvent: 'fetch-success' + }); + return 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: function(opts){ + var _this = this; + opts == null && (opts = {}); + opts = (__import({ + force: false, + readyIfError: false, + startEvent: 'load', + completeEvent: 'load-success', + errorEvent: 'load-error' + }, opts)); + if (opts.force) { + this.resetReady(); + } + if (!opts.start) { + throw new Error('You must specify a `start` function to start loading!'); + } + if (this.loading || this.ready) { + return this; + } + this.wait(); + this.loading = true; + this.trigger(opts.startEvent, this); + this.once(opts.completeEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.completeEvent !== 'load-success') { + _this.trigger('load-success', _this); + } + return _this.triggerReady(); + }); + this.once(opts.errorEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.errorEvent !== 'load-error') { + _this.trigger('load-error', _this); + } + if (opts.readyIfError) { + return _this.triggerReady(); + } + }); + opts.start.call(this); + return this; + } + /** + * Runs `.fetch()`, triggering a `fetch` event at start, and + * `fetch-success` / `fetch-error` on completion. + * + * @protected + * @returns {this} + */, + loadModel: function(){ + var _this = this; + this.wait(); + this.trigger('fetch', this); + this.fetch({ + success: function(){ + _this.unwait(); + return _this.trigger('fetch-success', _this); + }, + error: function(){ + _this.unwait(); + return _this.trigger.apply(_this, ['fetch-error', _this].concat(__slice.call(arguments))); + } + }); + return this; + }, + serialize: function(v){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return 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: function(opts){ + var kvo, k, v; + opts == null && (opts = {}); + opts = (__import({ + keepFunctions: false + }, opts)); + kvo = _.collapseObject(this.toJSON()); + for (k in kvo) { + v = kvo[k]; + if (opts.keepFunctions || typeof v !== 'function') { + kvo[k] = this.serialize(v); + } + } + return kvo; + } + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + } + /** + * @returns {String} URL identifying this model. + */, + toURL: function(){ + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "(cid=" + this.cid + ", id=" + this.id + ")"; + } +})); +__import(BaseModel, { + /** + * 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: function(o, item_delim, kv_delim){ + var Cls; + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + if (typeof o === 'string') { + o = _.fromKV(o, item_delim, kv_delim); + } + Cls = typeof this === 'function' + ? this + : this.constructor; + return 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({ + constructor: (function(){ + function BaseList(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Collection.apply(this, arguments); + } + return BaseList; + }()), + getIds: function(){ + return this.models.map(function(it){ + return it.id || it.get('id') || it.cid; + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.toJSON()); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "[" + this.length + "]"; + }, + toStringWithIds: function(){ + var modelIds; + modelIds = this.models.map(function(it){ + var _ref; + return "\"" + ((_ref = it.id) != null + ? _ref + : it.cid) + "\""; + }).join(', '); + return this.getClassName() + "[" + this.length + "](" + modelIds + ")"; + } +})); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/base-view.js b/lib/base/base-view.js new file mode 100644 index 0000000..9dec625 --- /dev/null +++ b/lib/base/base-view.js @@ -0,0 +1,330 @@ +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, DataBinding, BaseView, ViewList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +BaseModel = require('kraken/base/base-mixin').BaseModel; +DataBinding = require('kraken/base/data-binding').DataBinding; +/** + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View + */ +BaseView = exports.BaseView = Backbone.View.extend(mixinBase({ + 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(){ + function BaseView(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + this.subviews = new ViewList; + this.onReturnKeypress = _.debounce(this.onReturnKeypress.bind(this), 50); + Backbone.View.apply(this, arguments); + return this.trigger('create', this); + } + return BaseView; + }()), + initialize: function(){ + this.__apply_bind__(); + this.setModel(this.model); + this.build(); + return this.$el.on('form submit', function(it){ + return it.preventDefault(); + }); + }, + setModel: function(model){ + var data; + if (this.model) { + this.model.off('change', this.render, this); + this.model.off('destroy', this.remove, this); + delete this.model.view; + data = this.$el.data(); + delete data.model; + delete data.view; + } + if (this.model = model) { + this.model.view = this; + this.$el.data({ + model: this.model, + view: this + }); + this.model.on('change', this.render, this); + this.model.on('destroy', this.remove, this); + this.trigger('change:model', this, model); + } + return model; + }, + setParent: function(parent){ + var old_parent, _ref; + _ref = [this.parent, parent], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('parent', this, parent, old_parent); + return this; + }, + unsetParent: function(){ + var old_parent, _ref; + _ref = [this.parent, null], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('unparent', this, old_parent); + return this; + }, + addSubview: function(view){ + this.removeSubview(view); + this.subviews.push(view); + view.setParent(this); + return view; + }, + removeSubview: function(view){ + if (this.hasSubview(view)) { + view.remove(); + this.subviews.remove(view); + view.unsetParent(); + } + return view; + }, + hasSubview: function(view){ + return this.subviews.contains(view); + }, + invokeSubviews: function(){ + var _ref; + return (_ref = this.subviews).invoke.apply(_ref, arguments); + }, + removeAllSubviews: function(){ + this.subviews.forEach(this.removeSubview, this); + return this; + }, + attach: function(el){ + var _this = this; + this.$el.appendTo(el); + if (this.isAttached) { + return this; + } + this.isAttached = true; + _.delay(function(){ + _this.delegateEvents(); + return _this.trigger('attach', _this); + }, 50); + return this; + }, + remove: function(){ + this.$el.remove(); + if (!this.isAttached) { + return this; + } + this.isAttached = false; + this.trigger('unattach', this); + return this; + }, + clear: function(){ + this.remove(); + this.model.destroy(); + this.trigger('clear', this); + return this; + }, + hide: function(){ + this.$el.hide(); + this.trigger('hide', this); + return this; + }, + show: function(){ + this.$el.show(); + this.trigger('show', this); + return this; + } + /** + * Attach each subview to its bind-point. + * @returns {this} + */, + attachSubviews: function(){ + var bps, view, bp, _i, _ref, _len; + bps = this.getOwnSubviewBindPoints(); + if (this.subviews.length && !bps.length) { + console.warn(this + ".attachSubviews(): no subview bind-points found!"); + return this; + } + for (_i = 0, _len = (_ref = this.subviews).length; _i < _len; ++_i) { + view = _ref[_i]; + if (bp = this.findSubviewBindPoint(view, bps)) { + view.attach(bp); + } else { + console.warn(this + ".attachSubviews(): Unable to find bind-point for " + view + "!"); + } + } + return this; + } + /** + * Finds all subview bind-points under this view's element, but not under + * the view element of any subview. + * @returns {jQuery|undefined} + */, + getOwnSubviewBindPoints: function(){ + return this.$('[data-subview]').not(this.$('[data-subview] [data-subview]')); + } + /** + * Find the matching subview bind-point for the given view. + */, + findSubviewBindPoint: function(view, bind_points){ + var bp; + bind_points || (bind_points = this.getOwnSubviewBindPoints()); + if (view.id) { + bp = bind_points.filter("[data-subview$=':" + view.id + "']"); + if (bp.length) { + return bp.eq(0); + } + } + bp = bind_points.filter("[data-subview='" + view.getClassName() + "']"); + if (bp.length) { + return bp.eq(0); + } + }, + toTemplateLocals: function(){ + return this.model.toJSON(); + }, + $template: function(){ + return $(this.template((__import({ + _: _, + op: op, + model: this.model, + view: this + }, this.toTemplateLocals())))); + }, + build: function(){ + var outer; + if (!this.template) { + return this; + } + outer = this.$template(); + this.$el.html(outer.html()).attr({ + id: outer.attr('id'), + 'class': outer.attr('class') + }); + this.attachSubviews(); + this.isBuilt = true; + return this; + }, + render: function(){ + this.wait(); + if (this.isBuilt) { + this.update(); + } else { + this.build(); + } + this.renderSubviews(); + this.trigger('render', this); + this.unwait(); + return this; + }, + renderSubviews: function(){ + this.attachSubviews(); + this.subviews.invoke('render'); + return this; + }, + update: function(){ + var locals; + new DataBinding(this).update(locals = this.toTemplateLocals()); + this.trigger('update', this, locals); + return this; + } + /* * * * Events * * * */, + bubbleEventDown: function(evt){ + this.invokeSubviews.apply(this, ['trigger'].concat(__slice.call(arguments))); + return this; + }, + redispatch: function(evt){ + var args; + args = __slice.call(arguments, 1); + this.trigger.apply(this, [evt, this].concat(__slice.call(args))); + return this; + }, + onlyOnReturn: function(fn){ + var args, _this = this; + args = __slice.call(arguments, 1); + fn = _.debounce(fn.bind(this), 50); + return function(evt){ + if (evt.keyCode === 13) { + return fn.apply(_this, args); + } + }; + } + /** + * Call a delegate on keypress == the return key. + * @returns {Function} Keypress event handler. + */, + onReturnKeypress: function(evt){ + var fn; + if (this.callOnReturnKeypress) { + fn = this[this.callOnReturnKeypress]; + } + if (fn && evt.keyCode === 13) { + return fn.call(this); + } + }, + toString: function(){ + return this.getClassName() + "(model=" + this.model + ")"; + } +})); +['get', 'set', 'unset', 'toJSON', 'toKV', 'toURL'].forEach(function(methodname){ + return BaseView.prototype[methodname] = function(){ + return this.model[methodname].apply(this.model, arguments); + }; +}); +exports.ViewList = ViewList = (function(superclass){ + ViewList.displayName = 'ViewList'; + var prototype = __extend(ViewList, superclass).prototype, constructor = ViewList; + function ViewList(views){ + views == null && (views = []); + superclass.apply(this, arguments); + } + prototype.extend = function(views){ + var _this = this; + _.each(views, function(it){ + return _this.push(it); + }); + return this; + }; + prototype.findByModel = function(model){ + return this.find(function(it){ + return it.model === model; + }); + }; + prototype.toString = function(){ + var contents; + contents = this.length ? "\"" + this.join('","') + "\"" : ''; + return "ViewList[" + this.length + "](" + contents + ")"; + }; + return ViewList; +}(Array)); +['each', 'contains', 'invoke', 'pluck', 'find', 'remove', 'compact', 'flatten', 'without', 'union', 'intersection', 'difference', 'unique', 'uniq'].forEach(function(methodname){ + return ViewList.prototype[methodname] = function(){ + var _ref; + return (_ref = _[methodname]).call.apply(_ref, [_, this].concat(__slice.call(arguments))); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/base/base-view.mod.js b/lib/base/base-view.mod.js new file mode 100644 index 0000000..2105eea --- /dev/null +++ b/lib/base/base-view.mod.js @@ -0,0 +1,334 @@ +require.define('/node_modules/kraken/base/base-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, DataBinding, BaseView, ViewList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +BaseModel = require('kraken/base/base-mixin').BaseModel; +DataBinding = require('kraken/base/data-binding').DataBinding; +/** + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View + */ +BaseView = exports.BaseView = Backbone.View.extend(mixinBase({ + 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(){ + function BaseView(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + this.subviews = new ViewList; + this.onReturnKeypress = _.debounce(this.onReturnKeypress.bind(this), 50); + Backbone.View.apply(this, arguments); + return this.trigger('create', this); + } + return BaseView; + }()), + initialize: function(){ + this.__apply_bind__(); + this.setModel(this.model); + this.build(); + return this.$el.on('form submit', function(it){ + return it.preventDefault(); + }); + }, + setModel: function(model){ + var data; + if (this.model) { + this.model.off('change', this.render, this); + this.model.off('destroy', this.remove, this); + delete this.model.view; + data = this.$el.data(); + delete data.model; + delete data.view; + } + if (this.model = model) { + this.model.view = this; + this.$el.data({ + model: this.model, + view: this + }); + this.model.on('change', this.render, this); + this.model.on('destroy', this.remove, this); + this.trigger('change:model', this, model); + } + return model; + }, + setParent: function(parent){ + var old_parent, _ref; + _ref = [this.parent, parent], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('parent', this, parent, old_parent); + return this; + }, + unsetParent: function(){ + var old_parent, _ref; + _ref = [this.parent, null], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('unparent', this, old_parent); + return this; + }, + addSubview: function(view){ + this.removeSubview(view); + this.subviews.push(view); + view.setParent(this); + return view; + }, + removeSubview: function(view){ + if (this.hasSubview(view)) { + view.remove(); + this.subviews.remove(view); + view.unsetParent(); + } + return view; + }, + hasSubview: function(view){ + return this.subviews.contains(view); + }, + invokeSubviews: function(){ + var _ref; + return (_ref = this.subviews).invoke.apply(_ref, arguments); + }, + removeAllSubviews: function(){ + this.subviews.forEach(this.removeSubview, this); + return this; + }, + attach: function(el){ + var _this = this; + this.$el.appendTo(el); + if (this.isAttached) { + return this; + } + this.isAttached = true; + _.delay(function(){ + _this.delegateEvents(); + return _this.trigger('attach', _this); + }, 50); + return this; + }, + remove: function(){ + this.$el.remove(); + if (!this.isAttached) { + return this; + } + this.isAttached = false; + this.trigger('unattach', this); + return this; + }, + clear: function(){ + this.remove(); + this.model.destroy(); + this.trigger('clear', this); + return this; + }, + hide: function(){ + this.$el.hide(); + this.trigger('hide', this); + return this; + }, + show: function(){ + this.$el.show(); + this.trigger('show', this); + return this; + } + /** + * Attach each subview to its bind-point. + * @returns {this} + */, + attachSubviews: function(){ + var bps, view, bp, _i, _ref, _len; + bps = this.getOwnSubviewBindPoints(); + if (this.subviews.length && !bps.length) { + console.warn(this + ".attachSubviews(): no subview bind-points found!"); + return this; + } + for (_i = 0, _len = (_ref = this.subviews).length; _i < _len; ++_i) { + view = _ref[_i]; + if (bp = this.findSubviewBindPoint(view, bps)) { + view.attach(bp); + } else { + console.warn(this + ".attachSubviews(): Unable to find bind-point for " + view + "!"); + } + } + return this; + } + /** + * Finds all subview bind-points under this view's element, but not under + * the view element of any subview. + * @returns {jQuery|undefined} + */, + getOwnSubviewBindPoints: function(){ + return this.$('[data-subview]').not(this.$('[data-subview] [data-subview]')); + } + /** + * Find the matching subview bind-point for the given view. + */, + findSubviewBindPoint: function(view, bind_points){ + var bp; + bind_points || (bind_points = this.getOwnSubviewBindPoints()); + if (view.id) { + bp = bind_points.filter("[data-subview$=':" + view.id + "']"); + if (bp.length) { + return bp.eq(0); + } + } + bp = bind_points.filter("[data-subview='" + view.getClassName() + "']"); + if (bp.length) { + return bp.eq(0); + } + }, + toTemplateLocals: function(){ + return this.model.toJSON(); + }, + $template: function(){ + return $(this.template((__import({ + _: _, + op: op, + model: this.model, + view: this + }, this.toTemplateLocals())))); + }, + build: function(){ + var outer; + if (!this.template) { + return this; + } + outer = this.$template(); + this.$el.html(outer.html()).attr({ + id: outer.attr('id'), + 'class': outer.attr('class') + }); + this.attachSubviews(); + this.isBuilt = true; + return this; + }, + render: function(){ + this.wait(); + if (this.isBuilt) { + this.update(); + } else { + this.build(); + } + this.renderSubviews(); + this.trigger('render', this); + this.unwait(); + return this; + }, + renderSubviews: function(){ + this.attachSubviews(); + this.subviews.invoke('render'); + return this; + }, + update: function(){ + var locals; + new DataBinding(this).update(locals = this.toTemplateLocals()); + this.trigger('update', this, locals); + return this; + } + /* * * * Events * * * */, + bubbleEventDown: function(evt){ + this.invokeSubviews.apply(this, ['trigger'].concat(__slice.call(arguments))); + return this; + }, + redispatch: function(evt){ + var args; + args = __slice.call(arguments, 1); + this.trigger.apply(this, [evt, this].concat(__slice.call(args))); + return this; + }, + onlyOnReturn: function(fn){ + var args, _this = this; + args = __slice.call(arguments, 1); + fn = _.debounce(fn.bind(this), 50); + return function(evt){ + if (evt.keyCode === 13) { + return fn.apply(_this, args); + } + }; + } + /** + * Call a delegate on keypress == the return key. + * @returns {Function} Keypress event handler. + */, + onReturnKeypress: function(evt){ + var fn; + if (this.callOnReturnKeypress) { + fn = this[this.callOnReturnKeypress]; + } + if (fn && evt.keyCode === 13) { + return fn.call(this); + } + }, + toString: function(){ + return this.getClassName() + "(model=" + this.model + ")"; + } +})); +['get', 'set', 'unset', 'toJSON', 'toKV', 'toURL'].forEach(function(methodname){ + return BaseView.prototype[methodname] = function(){ + return this.model[methodname].apply(this.model, arguments); + }; +}); +exports.ViewList = ViewList = (function(superclass){ + ViewList.displayName = 'ViewList'; + var prototype = __extend(ViewList, superclass).prototype, constructor = ViewList; + function ViewList(views){ + views == null && (views = []); + superclass.apply(this, arguments); + } + prototype.extend = function(views){ + var _this = this; + _.each(views, function(it){ + return _this.push(it); + }); + return this; + }; + prototype.findByModel = function(model){ + return this.find(function(it){ + return it.model === model; + }); + }; + prototype.toString = function(){ + var contents; + contents = this.length ? "\"" + this.join('","') + "\"" : ''; + return "ViewList[" + this.length + "](" + contents + ")"; + }; + return ViewList; +}(Array)); +['each', 'contains', 'invoke', 'pluck', 'find', 'remove', 'compact', 'flatten', 'without', 'union', 'intersection', 'difference', 'unique', 'uniq'].forEach(function(methodname){ + return ViewList.prototype[methodname] = function(){ + var _ref; + return (_ref = _[methodname]).call.apply(_ref, [_, this].concat(__slice.call(arguments))); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/base/base.js b/lib/base/base.js new file mode 100644 index 0000000..e3f4bce --- /dev/null +++ b/lib/base/base.js @@ -0,0 +1,77 @@ +var EventEmitter, op, Base, k, _ref, _, _i, _len, __slice = [].slice; +EventEmitter = require('events').EventEmitter; +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; +EventEmitter.prototype.trigger = EventEmitter.prototype.emit; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Eventful base class. + * @extends EventEmitter + */ +Base = (function(superclass){ + /** + * 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 + */ + Base.displayName = 'Base'; + var prototype = __extend(Base, superclass).prototype, constructor = Base; + function Base(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.superclass; + this.__apply_bind__(); + superclass.call(this); + this.__class__.emit('new', this); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */ + prototype.__bind__ = []; + /** + * Applies the contents of `__bind__`. + */ + prototype.__apply_bind__ = function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + }; + prototype.getClassName = function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }; + prototype.toString = function(){ + return this.getClassName() + "()"; + }; + Base.extended = function(Subclass){ + var k, v, _own = {}.hasOwnProperty; + for (k in this) if (_own.call(this, k)) { + v = this[k]; + if (typeof v === 'function') { + Subclass[k] = v; + } + } + Subclass.__super__ = this.prototype; + return Subclass; + }; + return Base; +}(EventEmitter)); +for (_i = 0, _len = (_ref = ['getSuperClasses', 'pluckSuper', 'pluckSuperAndSelf']).length; _i < _len; ++_i) { + k = _ref[_i]; + Base[k] = Base.prototype[k] = _.methodize(_[k]); +} +__import(Base, EventEmitter.prototype); +module.exports = Base; +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base.mod.js b/lib/base/base.mod.js new file mode 100644 index 0000000..68ac0c5 --- /dev/null +++ b/lib/base/base.mod.js @@ -0,0 +1,81 @@ +require.define('/node_modules/kraken/base/base.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var EventEmitter, op, Base, k, _ref, _, _i, _len, __slice = [].slice; +EventEmitter = require('events').EventEmitter; +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; +EventEmitter.prototype.trigger = EventEmitter.prototype.emit; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Eventful base class. + * @extends EventEmitter + */ +Base = (function(superclass){ + /** + * 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 + */ + Base.displayName = 'Base'; + var prototype = __extend(Base, superclass).prototype, constructor = Base; + function Base(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.superclass; + this.__apply_bind__(); + superclass.call(this); + this.__class__.emit('new', this); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */ + prototype.__bind__ = []; + /** + * Applies the contents of `__bind__`. + */ + prototype.__apply_bind__ = function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + }; + prototype.getClassName = function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }; + prototype.toString = function(){ + return this.getClassName() + "()"; + }; + Base.extended = function(Subclass){ + var k, v, _own = {}.hasOwnProperty; + for (k in this) if (_own.call(this, k)) { + v = this[k]; + if (typeof v === 'function') { + Subclass[k] = v; + } + } + Subclass.__super__ = this.prototype; + return Subclass; + }; + return Base; +}(EventEmitter)); +for (_i = 0, _len = (_ref = ['getSuperClasses', 'pluckSuper', 'pluckSuperAndSelf']).length; _i < _len; ++_i) { + k = _ref[_i]; + Base[k] = Base.prototype[k] = _.methodize(_[k]); +} +__import(Base, EventEmitter.prototype); +module.exports = Base; +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/cascading-model.js b/lib/base/cascading-model.js new file mode 100644 index 0000000..8a1f89c --- /dev/null +++ b/lib/base/cascading-model.js @@ -0,0 +1,54 @@ +var op, BaseModel, BaseList, Cascade, CascadingModel, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-model'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Cascade = require('kraken/util/cascade'); +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend({ + /** + * The lookup cascade. + * @type Cascade + */ + cascade: null, + constructor: (function(){ + function CascadingModel(attributes, opts){ + attributes == null && (attributes = {}); + this.cascade = new Cascade(attributes); + return BaseModel.call(this, attributes, opts); + } + return CascadingModel; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + } + /** + * Recursively look up a (potenitally nested) attribute in the lookup chain. + * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys). + * @returns {*} + */, + get: function(key){ + return this.cascade.get(key); + }, + toJSON: function(opts){ + opts == null && (opts = {}); + opts = (__import({ + collapseCascade: false + }, opts)); + if (opts.collapseCascade) { + return this.cascade.collapse(); + } else { + return BaseModel.prototype.toJSON.apply(this, arguments); + } + } +}); +['addLookup', 'removeLookup', 'popLookup', 'shiftLookup', 'unshiftLookup', 'isOwnProperty', 'isOwnValue', 'isInheritedValue', 'isModifiedValue'].forEach(function(methodname){ + return CascadingModel.prototype[methodname] = function(){ + return this.cascade[methodname].apply(this.cascade, arguments); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/cascading-model.mod.js b/lib/base/cascading-model.mod.js new file mode 100644 index 0000000..5fc62d5 --- /dev/null +++ b/lib/base/cascading-model.mod.js @@ -0,0 +1,58 @@ +require.define('/node_modules/kraken/base/cascading-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseModel, BaseList, Cascade, CascadingModel, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-model'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Cascade = require('kraken/util/cascade'); +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend({ + /** + * The lookup cascade. + * @type Cascade + */ + cascade: null, + constructor: (function(){ + function CascadingModel(attributes, opts){ + attributes == null && (attributes = {}); + this.cascade = new Cascade(attributes); + return BaseModel.call(this, attributes, opts); + } + return CascadingModel; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + } + /** + * Recursively look up a (potenitally nested) attribute in the lookup chain. + * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys). + * @returns {*} + */, + get: function(key){ + return this.cascade.get(key); + }, + toJSON: function(opts){ + opts == null && (opts = {}); + opts = (__import({ + collapseCascade: false + }, opts)); + if (opts.collapseCascade) { + return this.cascade.collapse(); + } else { + return BaseModel.prototype.toJSON.apply(this, arguments); + } + } +}); +['addLookup', 'removeLookup', 'popLookup', 'shiftLookup', 'unshiftLookup', 'isOwnProperty', 'isOwnValue', 'isInheritedValue', 'isModifiedValue'].forEach(function(methodname){ + return CascadingModel.prototype[methodname] = function(){ + return this.cascade[methodname].apply(this.cascade, arguments); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/data-binding.js b/lib/base/data-binding.js new file mode 100644 index 0000000..9f1835c --- /dev/null +++ b/lib/base/data-binding.js @@ -0,0 +1,65 @@ +var Backbone, op, DataBinding, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +exports.DataBinding = DataBinding = (function(){ + DataBinding.displayName = 'DataBinding'; + var prototype = DataBinding.prototype, constructor = DataBinding; + prototype.data = null; + prototype.context = null; + prototype.el = null; + prototype.$el = null; + prototype.bindPoints = null; + function DataBinding(el, context){ + this.context = context != null ? context : el; + if (el instanceof Backbone.View) { + el = el.$el; + } + this.$el = $(el); + this.el = this.$el.get(0); + this.bindPoints = this.$('[data-bind], [name]').not(this.$('[data-subview]').find('[data-bind], [name]')); + } + prototype.$ = function(sel){ + return this.$el.find(sel); + }; + prototype.serialize = function(it){ + return it; + }; + prototype.update = function(data){ + var key, val, _ref; + this.data = data; + for (key in _ref = _.collapseObject(this.data)) { + val = _ref[key]; + this.updateBinding(key, val); + } + return this; + }; + prototype.updateBinding = function(key, val){ + var bp; + if (bp = this.findDataBindPoint(key)) { + if (_.isFunction(val)) { + val.call(this.context, val, key, bp, this.data); + } else if (bp.is('input:checkbox')) { + bp.attr('checked', !!val); + } else if (bp.is('input, textarea')) { + bp.val(this.serialize(val)); + } else { + if (op.toBool(bp.data('data-bind-escape'))) { + bp.text(this.serialize(val)); + } else { + bp.html(this.serialize(val)); + } + } + } else { + false && console.warn(this + ".updateBinding(): Unable to find data bind-point for " + key + "=" + val + "!"); + } + return this; + }; + prototype.findDataBindPoint = function(key){ + var bp; + bp = this.bindPoints.filter("[name='" + key + "'], [data-bind='" + key + "']"); + if (bp.length) { + return bp.eq(0); + } + }; + return DataBinding; +}()); \ No newline at end of file diff --git a/lib/base/data-binding.mod.js b/lib/base/data-binding.mod.js new file mode 100644 index 0000000..957bc2c --- /dev/null +++ b/lib/base/data-binding.mod.js @@ -0,0 +1,69 @@ +require.define('/node_modules/kraken/base/data-binding.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, DataBinding, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +exports.DataBinding = DataBinding = (function(){ + DataBinding.displayName = 'DataBinding'; + var prototype = DataBinding.prototype, constructor = DataBinding; + prototype.data = null; + prototype.context = null; + prototype.el = null; + prototype.$el = null; + prototype.bindPoints = null; + function DataBinding(el, context){ + this.context = context != null ? context : el; + if (el instanceof Backbone.View) { + el = el.$el; + } + this.$el = $(el); + this.el = this.$el.get(0); + this.bindPoints = this.$('[data-bind], [name]').not(this.$('[data-subview]').find('[data-bind], [name]')); + } + prototype.$ = function(sel){ + return this.$el.find(sel); + }; + prototype.serialize = function(it){ + return it; + }; + prototype.update = function(data){ + var key, val, _ref; + this.data = data; + for (key in _ref = _.collapseObject(this.data)) { + val = _ref[key]; + this.updateBinding(key, val); + } + return this; + }; + prototype.updateBinding = function(key, val){ + var bp; + if (bp = this.findDataBindPoint(key)) { + if (_.isFunction(val)) { + val.call(this.context, val, key, bp, this.data); + } else if (bp.is('input:checkbox')) { + bp.attr('checked', !!val); + } else if (bp.is('input, textarea')) { + bp.val(this.serialize(val)); + } else { + if (op.toBool(bp.data('data-bind-escape'))) { + bp.text(this.serialize(val)); + } else { + bp.html(this.serialize(val)); + } + } + } else { + false && console.warn(this + ".updateBinding(): Unable to find data bind-point for " + key + "=" + val + "!"); + } + return this; + }; + prototype.findDataBindPoint = function(key){ + var bp; + bp = this.bindPoints.filter("[name='" + key + "'], [data-bind='" + key + "']"); + if (bp.length) { + return bp.eq(0); + } + }; + return DataBinding; +}()); + +}); diff --git a/lib/base/index.js b/lib/base/index.js new file mode 100644 index 0000000..1c9458c --- /dev/null +++ b/lib/base/index.js @@ -0,0 +1,14 @@ +var mixins, models, views, cache, cascading, data_binding; +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'); +__import(__import(__import(__import(__import(__import(exports, mixins), models), views), cache), cascading), data_binding); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/index.mod.js b/lib/base/index.mod.js new file mode 100644 index 0000000..a599753 --- /dev/null +++ b/lib/base/index.mod.js @@ -0,0 +1,18 @@ +require.define('/node_modules/kraken/base/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var mixins, models, views, cache, cascading, data_binding; +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'); +__import(__import(__import(__import(__import(__import(exports, mixins), models), views), cache), cascading), data_binding); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/model-cache.js b/lib/base/model-cache.js new file mode 100644 index 0000000..f07c776 --- /dev/null +++ b/lib/base/model-cache.js @@ -0,0 +1,218 @@ +var Seq, ReadyEmitter, ModelCache, _; +_ = require('underscore'); +Seq = require('seq'); +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +/** + * @class Caches models and provides static lookups by ID. + */ +exports.ModelCache = ModelCache = (function(superclass){ + ModelCache.displayName = 'ModelCache'; + var prototype = __extend(ModelCache, superclass).prototype, constructor = ModelCache; + /** + * @see ReadyEmitter#readyEventName + * @private + * @constant + * @type String + */ + prototype.readyEventName = 'cache-ready'; + /** + * Default options. + * @private + * @constant + * @type Object + */ + prototype.DEFAULT_OPTIONS = { + ready: true, + cache: null, + create: null, + ModelType: null + }; + /** + * @private + * @type Object + */ + prototype.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 + */ + prototype.ModelType = null; + /** + * Collection holding the cached Models. + * @private + * @type Backbone.Collection + */ + prototype.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. + */; + function ModelCache(ModelType, options){ + var that, _ref; + if (!_.isFunction(ModelType)) { + _ref = [ModelType || {}, null], options = _ref[0], ModelType = _ref[1]; + } + this.options = (_ref = {}, __import(_ref, this.DEFAULT_OPTIONS), __import(_ref, options)); + this.cache = this.options.cache || new Backbone.Collection; + this.ModelType = ModelType || this.options.ModelType; + if (that = this.options.create) { + this.createModel = that; + } + this.ready = !!this.options.ready; + if (this.ModelType) { + this.decorate(this.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. + */ + prototype.createModel = function(id){ + return new this.ModelType({ + id: 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. + */ + prototype.register = function(model){ + if (this.cache.contains(model)) { + this.cache.remove(model, { + silent: true + }); + } + this.cache.add(model); + this.trigger('add', this, model); + return model; + }; + /** + * Synchronously check if a model is in the cache, returning it if so. + * + * @param {String} id The model ID to get. + * @returns {Model} + */ + prototype.get = function(id){ + return this.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} + */ + prototype.lookupAll = function(ids, cb, cxt){ + var _this = this; + cxt == null && (cxt = this); + if (!_.isArray(ids)) { + ids = [ids]; + } + if (!this.ready) { + this.on('cache-ready', function(){ + _this.off('cache-ready', arguments.callee); + return _this.lookupAll(ids, cb, cxt); + }); + return this; + } + Seq(ids).parMap_(function(next, id){ + var that; + if (that = _this.cache.get(id)) { + return next.ok(that); + } + return _this.register(_this.createModel(id)).on('ready', function(it){ + return next.ok(it); + }).load(); + }).unflatten().seq(function(models){ + return cb.call(cxt, null, models); + })['catch'](function(err){ + return cb.call(cxt, err); + }); + return 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} + */ + prototype.lookup = function(id, cb, cxt){ + cxt == null && (cxt = this); + return this.lookupAll([id], function(err, models){ + if (err) { + return cb.call(cxt, err); + } else { + return 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. + */ + prototype.decorate = function(obj){ + var m, _i, _ref, _len; + obj.__cache__ = this; + for (_i = 0, _len = (_ref = ['register', 'get', 'lookup', 'lookupAll']).length; _i < _len; ++_i) { + m = _ref[_i]; + obj[m] = this[m].bind(this); + } + return obj; + }; + prototype.toString = function(){ + return (this.constructor.displayName || this.constructor.name) + "(cache=" + this.cache + ")"; + }; + return ModelCache; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/model-cache.mod.js b/lib/base/model-cache.mod.js new file mode 100644 index 0000000..4e529f9 --- /dev/null +++ b/lib/base/model-cache.mod.js @@ -0,0 +1,222 @@ +require.define('/node_modules/kraken/base/model-cache.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, ReadyEmitter, ModelCache, _; +_ = require('underscore'); +Seq = require('seq'); +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +/** + * @class Caches models and provides static lookups by ID. + */ +exports.ModelCache = ModelCache = (function(superclass){ + ModelCache.displayName = 'ModelCache'; + var prototype = __extend(ModelCache, superclass).prototype, constructor = ModelCache; + /** + * @see ReadyEmitter#readyEventName + * @private + * @constant + * @type String + */ + prototype.readyEventName = 'cache-ready'; + /** + * Default options. + * @private + * @constant + * @type Object + */ + prototype.DEFAULT_OPTIONS = { + ready: true, + cache: null, + create: null, + ModelType: null + }; + /** + * @private + * @type Object + */ + prototype.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 + */ + prototype.ModelType = null; + /** + * Collection holding the cached Models. + * @private + * @type Backbone.Collection + */ + prototype.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. + */; + function ModelCache(ModelType, options){ + var that, _ref; + if (!_.isFunction(ModelType)) { + _ref = [ModelType || {}, null], options = _ref[0], ModelType = _ref[1]; + } + this.options = (_ref = {}, __import(_ref, this.DEFAULT_OPTIONS), __import(_ref, options)); + this.cache = this.options.cache || new Backbone.Collection; + this.ModelType = ModelType || this.options.ModelType; + if (that = this.options.create) { + this.createModel = that; + } + this.ready = !!this.options.ready; + if (this.ModelType) { + this.decorate(this.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. + */ + prototype.createModel = function(id){ + return new this.ModelType({ + id: 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. + */ + prototype.register = function(model){ + if (this.cache.contains(model)) { + this.cache.remove(model, { + silent: true + }); + } + this.cache.add(model); + this.trigger('add', this, model); + return model; + }; + /** + * Synchronously check if a model is in the cache, returning it if so. + * + * @param {String} id The model ID to get. + * @returns {Model} + */ + prototype.get = function(id){ + return this.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} + */ + prototype.lookupAll = function(ids, cb, cxt){ + var _this = this; + cxt == null && (cxt = this); + if (!_.isArray(ids)) { + ids = [ids]; + } + if (!this.ready) { + this.on('cache-ready', function(){ + _this.off('cache-ready', arguments.callee); + return _this.lookupAll(ids, cb, cxt); + }); + return this; + } + Seq(ids).parMap_(function(next, id){ + var that; + if (that = _this.cache.get(id)) { + return next.ok(that); + } + return _this.register(_this.createModel(id)).on('ready', function(it){ + return next.ok(it); + }).load(); + }).unflatten().seq(function(models){ + return cb.call(cxt, null, models); + })['catch'](function(err){ + return cb.call(cxt, err); + }); + return 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} + */ + prototype.lookup = function(id, cb, cxt){ + cxt == null && (cxt = this); + return this.lookupAll([id], function(err, models){ + if (err) { + return cb.call(cxt, err); + } else { + return 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. + */ + prototype.decorate = function(obj){ + var m, _i, _ref, _len; + obj.__cache__ = this; + for (_i = 0, _len = (_ref = ['register', 'get', 'lookup', 'lookupAll']).length; _i < _len; ++_i) { + m = _ref[_i]; + obj[m] = this[m].bind(this); + } + return obj; + }; + prototype.toString = function(){ + return (this.constructor.displayName || this.constructor.name) + "(cache=" + this.cache + ")"; + }; + return ModelCache; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/index.js b/lib/base/scaffold/index.js new file mode 100644 index 0000000..c0ee89d --- /dev/null +++ b/lib/base/scaffold/index.js @@ -0,0 +1,9 @@ +var models, views; +models = require('kraken/base/scaffold/scaffold-model'); +views = require('kraken/base/scaffold/scaffold-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/scaffold/index.mod.js b/lib/base/scaffold/index.mod.js new file mode 100644 index 0000000..2278e76 --- /dev/null +++ b/lib/base/scaffold/index.mod.js @@ -0,0 +1,13 @@ +require.define('/node_modules/kraken/base/scaffold/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var models, views; +models = require('kraken/base/scaffold/scaffold-model'); +views = require('kraken/base/scaffold/scaffold-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/scaffold-model.js b/lib/base/scaffold/scaffold-model.js new file mode 100644 index 0000000..948a703 --- /dev/null +++ b/lib/base/scaffold/scaffold-model.js @@ -0,0 +1,131 @@ +var op, BaseModel, BaseList, Field, FieldList, _, _ref, __slice = [].slice; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Field = exports.Field = BaseModel.extend({ + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function Field(){ + return BaseModel.apply(this, arguments); + } + return Field; + }()), + initialize: function(){ + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + return Field.__super__.initialize.apply(this, arguments); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serializers * * */, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +FieldList = exports.FieldList = BaseList.extend({ + model: Field, + constructor: (function(){ + function FieldList(){ + return BaseList.apply(this, arguments); + } + return FieldList; + }()) + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */, + values: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: true, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/scaffold/scaffold-model.mod.js b/lib/base/scaffold/scaffold-model.mod.js new file mode 100644 index 0000000..ee06f50 --- /dev/null +++ b/lib/base/scaffold/scaffold-model.mod.js @@ -0,0 +1,135 @@ +require.define('/node_modules/kraken/base/scaffold/scaffold-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseModel, BaseList, Field, FieldList, _, _ref, __slice = [].slice; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Field = exports.Field = BaseModel.extend({ + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function Field(){ + return BaseModel.apply(this, arguments); + } + return Field; + }()), + initialize: function(){ + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + return Field.__super__.initialize.apply(this, arguments); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serializers * * */, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +FieldList = exports.FieldList = BaseList.extend({ + model: Field, + constructor: (function(){ + function FieldList(){ + return BaseList.apply(this, arguments); + } + return FieldList; + }()) + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */, + values: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: true, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/scaffold-view.js b/lib/base/scaffold/scaffold-view.js new file mode 100644 index 0000000..2bc3791 --- /dev/null +++ b/lib/base/scaffold/scaffold-view.js @@ -0,0 +1,116 @@ +var op, BaseView, Field, FieldList, FieldView, Scaffold, _, _ref; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/base/scaffold/scaffold-model'), Field = _ref.Field, FieldList = _ref.FieldList; +FieldView = exports.FieldView = BaseView.extend({ + tagName: 'div', + className: 'field', + type: 'string', + events: { + 'blur .value': 'onChange', + 'submit .value': 'onChange' + }, + constructor: (function(){ + function FieldView(){ + return BaseView.apply(this, arguments); + } + return FieldView; + }()), + initialize: function(){ + BaseView.prototype.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + }, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this); + }, + toTemplateLocals: function(){ + var json, v; + json = FieldView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + if (v = json.value && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return json; + } + /** + * A ghetto default template, typically overridden by superclass. + */, + template: function(locals){ + return $("\n"); + }, + render: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + return FieldView.__super__.render.apply(this, arguments); + } +}); +Scaffold = exports.Scaffold = BaseView.extend({ + __bind__: ['addField', 'resetFields'], + tagName: 'form', + className: 'scaffold', + collectionType: FieldList, + subviewType: FieldView, + constructor: (function(){ + function Scaffold(){ + return BaseView.apply(this, arguments); + } + return Scaffold; + }()), + initialize: function(){ + var CollectionType; + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + BaseView.prototype.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + return this.collection.on('reset', this.resetFields, this); + }, + addField: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + view = this.addSubview(new SubviewType({ + model: field + })); + view.on('change', this.onChange.bind(this, field)); + this.render(); + return view; + }, + resetFields: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return this; + }, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return Scaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); \ No newline at end of file diff --git a/lib/base/scaffold/scaffold-view.mod.js b/lib/base/scaffold/scaffold-view.mod.js new file mode 100644 index 0000000..f71a8da --- /dev/null +++ b/lib/base/scaffold/scaffold-view.mod.js @@ -0,0 +1,120 @@ +require.define('/node_modules/kraken/base/scaffold/scaffold-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseView, Field, FieldList, FieldView, Scaffold, _, _ref; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/base/scaffold/scaffold-model'), Field = _ref.Field, FieldList = _ref.FieldList; +FieldView = exports.FieldView = BaseView.extend({ + tagName: 'div', + className: 'field', + type: 'string', + events: { + 'blur .value': 'onChange', + 'submit .value': 'onChange' + }, + constructor: (function(){ + function FieldView(){ + return BaseView.apply(this, arguments); + } + return FieldView; + }()), + initialize: function(){ + BaseView.prototype.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + }, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this); + }, + toTemplateLocals: function(){ + var json, v; + json = FieldView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + if (v = json.value && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return json; + } + /** + * A ghetto default template, typically overridden by superclass. + */, + template: function(locals){ + return $("\n"); + }, + render: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + return FieldView.__super__.render.apply(this, arguments); + } +}); +Scaffold = exports.Scaffold = BaseView.extend({ + __bind__: ['addField', 'resetFields'], + tagName: 'form', + className: 'scaffold', + collectionType: FieldList, + subviewType: FieldView, + constructor: (function(){ + function Scaffold(){ + return BaseView.apply(this, arguments); + } + return Scaffold; + }()), + initialize: function(){ + var CollectionType; + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + BaseView.prototype.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + return this.collection.on('reset', this.resetFields, this); + }, + addField: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + view = this.addSubview(new SubviewType({ + model: field + })); + view.on('change', this.onChange.bind(this, field)); + this.render(); + return view; + }, + resetFields: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return this; + }, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return Scaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); + +}); diff --git a/lib/chart/chart-type.js b/lib/chart/chart-type.js new file mode 100644 index 0000000..037682a --- /dev/null +++ b/lib/chart/chart-type.js @@ -0,0 +1,435 @@ +var moment, Backbone, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; +moment = require('moment'); +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; +/** + * 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 + */ +exports.ChartType = ChartType = (function(superclass){ + /** + * Register a new chart type. + */ + ChartType.displayName = 'ChartType'; + var prototype = __extend(ChartType, superclass).prototype, constructor = ChartType; + ChartType.register = function(Subclass){ + return KNOWN_CHART_TYPES[Subclass.prototype.typeName] = Subclass; + }; + /** + * Look up a `ChartType` by `typeName`. + */ + ChartType.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartType'); + } + return KNOWN_CHART_TYPES[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {ChartType} + */ + ChartType.create = function(model, view){ + var Type; + if (!(Type = this.lookup(model))) { + return null; + } + return new Type(model, view); + }; + /* + * 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 + */ + prototype.SPEC_URL = null; + /** + * Chart-type name. + * @type String + * @readonly + */ + prototype.typeName = null; + /** + * Map of option name to ChartOption objects. + * @type { name:ChartOption, ... } + * @readonly + */ + prototype.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 + */ + prototype.options_ordered = null; + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport' + }; + /** + * Whether the ChartType has loaded all its data and is ready. + * @type Boolean + */ + prototype.ready = false; + /** + * Model to be rendered as a chart. + * @type Backbone.Model + */ + prototype.model = null; + /** + * View to render the chart into. + * @type Backbone.View + */ + prototype.view = null; + /** + * Last chart rendered by this ChartType. + * @private + */ + prototype.chart = null; + /** + * @constructor + */; + function ChartType(model, view){ + this.model = model; + this.view = view; + this.roles || (this.roles = {}); + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + } + prototype.withModel = function(model){ + this.model = model; + return this; + }; + prototype.withView = function(view){ + this.view = view; + return this; + }; + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + prototype.loadSpec = function(){ + var proto, _this = this; + if (this.ready) { + return this; + } + proto = this.constructor.prototype; + jQuery.ajax({ + url: this.SPEC_URL, + dataType: 'json', + success: function(spec){ + proto.spec = spec; + proto.options_ordered = spec; + proto.options = _.synthesize(spec, function(it){ + return [it.name, it]; + }); + proto.ready = true; + return _this.triggerReady(); + }, + error: function(it){ + return console.error("Error loading " + _this.typeName + " spec! " + it); + } + }); + return this; + }; + /** + * @returns {ChartOption} Get an option's spec by name. + */ + prototype.getOption = function(name, def){ + return this.options[name] || def; + }; + /** + * @returns {Object} An object, mapping from option.name to the + * result of the supplied function. + */ + prototype.map = function(fn, context){ + var _this = this; + context == null && (context = this); + return _.synthesize(this.options, function(it){ + return [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. + */ + prototype.pluck = function(attr){ + return this.map(function(it){ + return it[attr]; + }); + }; + /** + * @returns {Boolean} Whether the supplied value is the same as + * the default value for the given key. + */ + prototype.isDefault = function(name, value){ + return _.isEqual(this.getOption(name)['default'], value); + }; + /** + * When implementing a ChartType, you can add or override parsers + * merely by subclassing. + * @borrows ParserMixin + */; + ParserMixin.mix(ChartType); + /** + * @returns {Function} Parser for the given option name. + */ + prototype.getParserFor = function(name){ + return this.getParser(this.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. + */ + prototype.parseOption = function(name, value){ + return this.getParserFor(name)(value); + }; + /** + * Parses options using `parseOption(name, value)`. + * + * @param {Object} options Options to parse. + * @returns {Object} Parsed options. + */ + prototype.parseOptions = function(options){ + var out, k, v; + out = {}; + for (k in options) { + v = options[k]; + out[k] = this.parseOption(k, v); + } + return 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 + */ + prototype.serialize = function(v, k){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return String(v); + }; + /** + * Formats a date for display on an axis: `MM/YYYY` + * @param {Date} d Date to format. + * @returns {String} + */ + prototype.axisDateFormatter = function(d){ + return moment(d).format('MM/YYYY'); + }; + /** + * Formats a date for display in the legend: `DD MMM YYYY` + * @param {Date} d Date to format. + * @returns {String} + */ + prototype.dateFormatter = function(d){ + return 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. + */ + prototype.numberFormatter = function(n, digits, abbrev){ + var suffixes, suffix, d, s, parts, whole, fraction, _i, _len, _ref; + digits == null && (digits = 2); + abbrev == null && (abbrev = true); + suffixes = abbrev + ? [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + : [['Billion', 1000000000], ['Million', 1000000], ['', NaN]]; + for (_i = 0, _len = suffixes.length; _i < _len; ++_i) { + _ref = suffixes[_i], suffix = _ref[0], d = _ref[1]; + if (isNaN(d)) { + break; + } + 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('.'); + return { + n: n, + digits: digits, + whole: whole, + fraction: fraction, + suffix: suffix, + toString: function(){ + return this.whole + "" + this.fraction + (abbrev ? '' : ' ') + this.suffix; + } + }; + }; + /** + * 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. + */ + prototype.getElementsForRole = function(role){ + var that; + if (!this.view) { + return null; + } + if (that = this.roles[role]) { + return this.view.$(that); + } else { + return null; + } + }; + /** + * Transform/extract the data for this chart from the model. Default + * implementation calls `model.getData()`. + * + * @returns {*} Data object for the chart. + */ + prototype.getData = function(){ + return this.model.getData(); + }; + /** + * Map from option-name to default value. Note that this reference will be + * modified by `.render()`. + * + * @returns {Object} Default options. + */ + prototype.getDefaultOptions = function(){ + return this.pluck('default'); + }; + /** + * Resizes the HTML viewport. Override to disable, etc. + */ + prototype.resizeViewport = function(){ + var size; + size = this.determineSize(); + this.getElementsForRole('viewport').css(size); + return size; + }; + /** + * Determines chart viewport size. + * @return { width, height } + */ + prototype.determineSize = function(){ + var width, modelW, height, modelH, viewport, Width; + modelW = width = this.model.get('width'); + modelH = height = this.model.get('height'); + if (!(this.view.ready && width && height)) { + return { + width: width, + height: height + }; + } + viewport = this.getElementsForRole('viewport'); + if (width === 'auto') { + Width = viewport.innerWidth() || 300; + } + width == null && (width = modelW); + if (height === 'auto') { + height = viewport.innerHeight() || 320; + } + height == null && (height = modelH); + return { + width: width, + height: height + }; + }; + /** + * Transforms domain data and applies it to the chart library to + * render or update the corresponding chart. + * + * @returns {Chart} + */ + prototype.render = function(){ + var data, options, viewport; + data = this.getData(); + options = __import(this.getDefaultOptions(), this.transform(this.model, this.view)); + viewport = this.getElementsForRole('viewport'); + if (!((data != null && data.length) && (viewport != null && viewport.length))) { + return this.lastChart; + } + return this.lastChart = this.renderChart(data, viewport, options, this.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. + */ + prototype.transform = function(model, view){ + return this.model.getOptions(); + }; + /** + * Called to render the chart. + * + * @abstract + * @returns {Chart} + */ + prototype.renderChart = function(data, viewport, options, lastChart){ + throw Error('unimplemented'); + }; + return ChartType; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/chart-type.mod.js b/lib/chart/chart-type.mod.js new file mode 100644 index 0000000..40aaea3 --- /dev/null +++ b/lib/chart/chart-type.mod.js @@ -0,0 +1,439 @@ +require.define('/node_modules/kraken/chart/chart-type.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var moment, Backbone, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; +moment = require('moment'); +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; +/** + * 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 + */ +exports.ChartType = ChartType = (function(superclass){ + /** + * Register a new chart type. + */ + ChartType.displayName = 'ChartType'; + var prototype = __extend(ChartType, superclass).prototype, constructor = ChartType; + ChartType.register = function(Subclass){ + return KNOWN_CHART_TYPES[Subclass.prototype.typeName] = Subclass; + }; + /** + * Look up a `ChartType` by `typeName`. + */ + ChartType.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartType'); + } + return KNOWN_CHART_TYPES[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {ChartType} + */ + ChartType.create = function(model, view){ + var Type; + if (!(Type = this.lookup(model))) { + return null; + } + return new Type(model, view); + }; + /* + * 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 + */ + prototype.SPEC_URL = null; + /** + * Chart-type name. + * @type String + * @readonly + */ + prototype.typeName = null; + /** + * Map of option name to ChartOption objects. + * @type { name:ChartOption, ... } + * @readonly + */ + prototype.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 + */ + prototype.options_ordered = null; + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport' + }; + /** + * Whether the ChartType has loaded all its data and is ready. + * @type Boolean + */ + prototype.ready = false; + /** + * Model to be rendered as a chart. + * @type Backbone.Model + */ + prototype.model = null; + /** + * View to render the chart into. + * @type Backbone.View + */ + prototype.view = null; + /** + * Last chart rendered by this ChartType. + * @private + */ + prototype.chart = null; + /** + * @constructor + */; + function ChartType(model, view){ + this.model = model; + this.view = view; + this.roles || (this.roles = {}); + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + } + prototype.withModel = function(model){ + this.model = model; + return this; + }; + prototype.withView = function(view){ + this.view = view; + return this; + }; + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + prototype.loadSpec = function(){ + var proto, _this = this; + if (this.ready) { + return this; + } + proto = this.constructor.prototype; + jQuery.ajax({ + url: this.SPEC_URL, + dataType: 'json', + success: function(spec){ + proto.spec = spec; + proto.options_ordered = spec; + proto.options = _.synthesize(spec, function(it){ + return [it.name, it]; + }); + proto.ready = true; + return _this.triggerReady(); + }, + error: function(it){ + return console.error("Error loading " + _this.typeName + " spec! " + it); + } + }); + return this; + }; + /** + * @returns {ChartOption} Get an option's spec by name. + */ + prototype.getOption = function(name, def){ + return this.options[name] || def; + }; + /** + * @returns {Object} An object, mapping from option.name to the + * result of the supplied function. + */ + prototype.map = function(fn, context){ + var _this = this; + context == null && (context = this); + return _.synthesize(this.options, function(it){ + return [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. + */ + prototype.pluck = function(attr){ + return this.map(function(it){ + return it[attr]; + }); + }; + /** + * @returns {Boolean} Whether the supplied value is the same as + * the default value for the given key. + */ + prototype.isDefault = function(name, value){ + return _.isEqual(this.getOption(name)['default'], value); + }; + /** + * When implementing a ChartType, you can add or override parsers + * merely by subclassing. + * @borrows ParserMixin + */; + ParserMixin.mix(ChartType); + /** + * @returns {Function} Parser for the given option name. + */ + prototype.getParserFor = function(name){ + return this.getParser(this.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. + */ + prototype.parseOption = function(name, value){ + return this.getParserFor(name)(value); + }; + /** + * Parses options using `parseOption(name, value)`. + * + * @param {Object} options Options to parse. + * @returns {Object} Parsed options. + */ + prototype.parseOptions = function(options){ + var out, k, v; + out = {}; + for (k in options) { + v = options[k]; + out[k] = this.parseOption(k, v); + } + return 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 + */ + prototype.serialize = function(v, k){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return String(v); + }; + /** + * Formats a date for display on an axis: `MM/YYYY` + * @param {Date} d Date to format. + * @returns {String} + */ + prototype.axisDateFormatter = function(d){ + return moment(d).format('MM/YYYY'); + }; + /** + * Formats a date for display in the legend: `DD MMM YYYY` + * @param {Date} d Date to format. + * @returns {String} + */ + prototype.dateFormatter = function(d){ + return 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. + */ + prototype.numberFormatter = function(n, digits, abbrev){ + var suffixes, suffix, d, s, parts, whole, fraction, _i, _len, _ref; + digits == null && (digits = 2); + abbrev == null && (abbrev = true); + suffixes = abbrev + ? [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + : [['Billion', 1000000000], ['Million', 1000000], ['', NaN]]; + for (_i = 0, _len = suffixes.length; _i < _len; ++_i) { + _ref = suffixes[_i], suffix = _ref[0], d = _ref[1]; + if (isNaN(d)) { + break; + } + 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('.'); + return { + n: n, + digits: digits, + whole: whole, + fraction: fraction, + suffix: suffix, + toString: function(){ + return this.whole + "" + this.fraction + (abbrev ? '' : ' ') + this.suffix; + } + }; + }; + /** + * 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. + */ + prototype.getElementsForRole = function(role){ + var that; + if (!this.view) { + return null; + } + if (that = this.roles[role]) { + return this.view.$(that); + } else { + return null; + } + }; + /** + * Transform/extract the data for this chart from the model. Default + * implementation calls `model.getData()`. + * + * @returns {*} Data object for the chart. + */ + prototype.getData = function(){ + return this.model.getData(); + }; + /** + * Map from option-name to default value. Note that this reference will be + * modified by `.render()`. + * + * @returns {Object} Default options. + */ + prototype.getDefaultOptions = function(){ + return this.pluck('default'); + }; + /** + * Resizes the HTML viewport. Override to disable, etc. + */ + prototype.resizeViewport = function(){ + var size; + size = this.determineSize(); + this.getElementsForRole('viewport').css(size); + return size; + }; + /** + * Determines chart viewport size. + * @return { width, height } + */ + prototype.determineSize = function(){ + var width, modelW, height, modelH, viewport, Width; + modelW = width = this.model.get('width'); + modelH = height = this.model.get('height'); + if (!(this.view.ready && width && height)) { + return { + width: width, + height: height + }; + } + viewport = this.getElementsForRole('viewport'); + if (width === 'auto') { + Width = viewport.innerWidth() || 300; + } + width == null && (width = modelW); + if (height === 'auto') { + height = viewport.innerHeight() || 320; + } + height == null && (height = modelH); + return { + width: width, + height: height + }; + }; + /** + * Transforms domain data and applies it to the chart library to + * render or update the corresponding chart. + * + * @returns {Chart} + */ + prototype.render = function(){ + var data, options, viewport; + data = this.getData(); + options = __import(this.getDefaultOptions(), this.transform(this.model, this.view)); + viewport = this.getElementsForRole('viewport'); + if (!((data != null && data.length) && (viewport != null && viewport.length))) { + return this.lastChart; + } + return this.lastChart = this.renderChart(data, viewport, options, this.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. + */ + prototype.transform = function(model, view){ + return this.model.getOptions(); + }; + /** + * Called to render the chart. + * + * @abstract + * @returns {Chart} + */ + prototype.renderChart = function(data, viewport, options, lastChart){ + throw Error('unimplemented'); + }; + return ChartType; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/index.js b/lib/chart/index.js new file mode 100644 index 0000000..21c43f1 --- /dev/null +++ b/lib/chart/index.js @@ -0,0 +1,12 @@ +var chart_type, chart_option, dygraphs, d3_chart, d3_elements; +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'); +__import(__import(__import(__import(__import(exports, chart_type), chart_option), dygraphs), d3_chart), d3_elements); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/index.mod.js b/lib/chart/index.mod.js new file mode 100644 index 0000000..7734fba --- /dev/null +++ b/lib/chart/index.mod.js @@ -0,0 +1,16 @@ +require.define('/node_modules/kraken/chart/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var chart_type, chart_option, dygraphs, d3_chart, d3_elements; +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'); +__import(__import(__import(__import(__import(exports, chart_type), chart_option), dygraphs), d3_chart), d3_elements); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/option/chart-option-model.js b/lib/chart/option/chart-option-model.js new file mode 100644 index 0000000..4b7382f --- /dev/null +++ b/lib/chart/option/chart-option-model.js @@ -0,0 +1,256 @@ +var op, Parsers, ParserMixin, ParsingModel, ParsingView, BaseModel, BaseList, TagSet, KNOWN_TAGS, ChartOption, ChartOptionList, _ref, _, __slice = [].slice; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin, ParsingModel = _ref.ParsingModel, ParsingView = _ref.ParsingView; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +/** + * @class A set of tags. + */ +exports.TagSet = TagSet = (function(superclass){ + TagSet.displayName = 'TagSet'; + var prototype = __extend(TagSet, superclass).prototype, constructor = TagSet; + prototype.tags = {}; + function TagSet(values){ + values == null && (values = []); + this.tags = {}; + if (values != null && values.length) { + this.add(values); + } + } + prototype.has = function(tag){ + return this.tags[tag] != null; + }; + prototype.get = function(tag){ + if (!tag) { + return -1; + } + if (this.tags[tag] == null) { + this.tags[tag] = this.length; + this.push(tag); + } + return this.tags[tag]; + }; + prototype.update = function(tags){ + var is_single, tag, indices, _res, _i, _len; + is_single = typeof tags === 'string'; + if (is_single) { + tags = [tags]; + } + _res = []; + for (_i = 0, _len = tags.length; _i < _len; ++_i) { + tag = tags[_i]; + _res.push(this.get(tag)); + } + indices = _res; + if (is_single) { + return indices[0]; + } else { + return indices; + } + }; + prototype.toString = function(){ + return "TagSet(length=" + this.length + ", values=[\"" + this.join('", "') + "\"])"; + }; + return TagSet; +}(Array)); +/** + * @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({ + IGNORED_TAGS: ['callback', 'deprecated', 'debugging'], + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function ChartOption(){ + return ParsingModel.apply(this, arguments); + } + return ChartOption; + }()), + initialize: function(){ + var type, tags; + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + ChartOption.__super__.initialize.apply(this, arguments); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + KNOWN_TAGS.update(this.getCategory()); + type = this.get('type').toLowerCase() || ''; + tags = this.get('tags') || []; + if (_.str.include(type, 'function') || _.intersection(tags, this.IGNORED_TAGS).length) { + return this.set('ignore', true); + } + }, + addTag: function(tag){ + var tags; + if (!tag) { + return this; + } + tags = this.get('tags') || []; + tags.push(tag); + this.set('tags', tags); + return this; + }, + removeTag: function(tag){ + var tags; + if (!tag) { + return this; + } + tags = this.get('tags') || []; + _.remove(tags, tag); + this.set('tags', tags); + return this; + }, + onTagUpdate: function(){ + KNOWN_TAGS.update(this.get('tags')); + return this; + }, + getTagIndex: function(tag){ + return KNOWN_TAGS.get(tag); + }, + getCategory: function(){ + var tags; + return tags = (this.get('tags') || [])[0]; + }, + getCategoryIndex: function(){ + return this.getTagIndex(this.getCategory()); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serialization * * */ + /** + * Override to default `type` to the model attribute of the same name. + * @returns {Function} Parser for the given type. + */, + getParser: function(type){ + type || (type = this.get('type') || 'String'); + return ChartOption.__super__.getParser.call(this, type); + }, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +/** + * @class List of ChartOption fields. + */ +ChartOptionList = exports.ChartOptionList = BaseList.extend({ + model: ChartOption, + constructor: (function(){ + function ChartOptionList(){ + return BaseList.apply(this, arguments); + } + return ChartOptionList; + }()) + /** + * 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: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + } + /** + * Override to omit defaults from URL. + */, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: false, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/option/chart-option-model.mod.js b/lib/chart/option/chart-option-model.mod.js new file mode 100644 index 0000000..50446ba --- /dev/null +++ b/lib/chart/option/chart-option-model.mod.js @@ -0,0 +1,260 @@ +require.define('/node_modules/kraken/chart/option/chart-option-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, Parsers, ParserMixin, ParsingModel, ParsingView, BaseModel, BaseList, TagSet, KNOWN_TAGS, ChartOption, ChartOptionList, _ref, _, __slice = [].slice; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin, ParsingModel = _ref.ParsingModel, ParsingView = _ref.ParsingView; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +/** + * @class A set of tags. + */ +exports.TagSet = TagSet = (function(superclass){ + TagSet.displayName = 'TagSet'; + var prototype = __extend(TagSet, superclass).prototype, constructor = TagSet; + prototype.tags = {}; + function TagSet(values){ + values == null && (values = []); + this.tags = {}; + if (values != null && values.length) { + this.add(values); + } + } + prototype.has = function(tag){ + return this.tags[tag] != null; + }; + prototype.get = function(tag){ + if (!tag) { + return -1; + } + if (this.tags[tag] == null) { + this.tags[tag] = this.length; + this.push(tag); + } + return this.tags[tag]; + }; + prototype.update = function(tags){ + var is_single, tag, indices, _res, _i, _len; + is_single = typeof tags === 'string'; + if (is_single) { + tags = [tags]; + } + _res = []; + for (_i = 0, _len = tags.length; _i < _len; ++_i) { + tag = tags[_i]; + _res.push(this.get(tag)); + } + indices = _res; + if (is_single) { + return indices[0]; + } else { + return indices; + } + }; + prototype.toString = function(){ + return "TagSet(length=" + this.length + ", values=[\"" + this.join('", "') + "\"])"; + }; + return TagSet; +}(Array)); +/** + * @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({ + IGNORED_TAGS: ['callback', 'deprecated', 'debugging'], + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function ChartOption(){ + return ParsingModel.apply(this, arguments); + } + return ChartOption; + }()), + initialize: function(){ + var type, tags; + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + ChartOption.__super__.initialize.apply(this, arguments); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + KNOWN_TAGS.update(this.getCategory()); + type = this.get('type').toLowerCase() || ''; + tags = this.get('tags') || []; + if (_.str.include(type, 'function') || _.intersection(tags, this.IGNORED_TAGS).length) { + return this.set('ignore', true); + } + }, + addTag: function(tag){ + var tags; + if (!tag) { + return this; + } + tags = this.get('tags') || []; + tags.push(tag); + this.set('tags', tags); + return this; + }, + removeTag: function(tag){ + var tags; + if (!tag) { + return this; + } + tags = this.get('tags') || []; + _.remove(tags, tag); + this.set('tags', tags); + return this; + }, + onTagUpdate: function(){ + KNOWN_TAGS.update(this.get('tags')); + return this; + }, + getTagIndex: function(tag){ + return KNOWN_TAGS.get(tag); + }, + getCategory: function(){ + var tags; + return tags = (this.get('tags') || [])[0]; + }, + getCategoryIndex: function(){ + return this.getTagIndex(this.getCategory()); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serialization * * */ + /** + * Override to default `type` to the model attribute of the same name. + * @returns {Function} Parser for the given type. + */, + getParser: function(type){ + type || (type = this.get('type') || 'String'); + return ChartOption.__super__.getParser.call(this, type); + }, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +/** + * @class List of ChartOption fields. + */ +ChartOptionList = exports.ChartOptionList = BaseList.extend({ + model: ChartOption, + constructor: (function(){ + function ChartOptionList(){ + return BaseList.apply(this, arguments); + } + return ChartOptionList; + }()) + /** + * 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: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + } + /** + * Override to omit defaults from URL. + */, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: false, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/option/chart-option-view.js b/lib/chart/option/chart-option-view.js new file mode 100644 index 0000000..99a9a41 --- /dev/null +++ b/lib/chart/option/chart-option-view.js @@ -0,0 +1,256 @@ +var op, BaseView, ChartOption, ChartOptionList, DEBOUNCE_RENDER, ChartOptionView, ChartOptionScaffold, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/chart/option/chart-option-model'), ChartOption = _ref.ChartOption, ChartOptionList = _ref.ChartOptionList; +DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100; +/** + * @class View for a single configurable option in a chart type. + */ +ChartOptionView = exports.ChartOptionView = BaseView.extend({ + 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(){ + function ChartOptionView(){ + return BaseView.apply(this, arguments); + } + return ChartOptionView; + }()), + initialize: function(){ + ChartOptionView.__super__.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + } + /* * * * Rendering * * * */, + toTemplateLocals: function(){ + var json, v; + json = ChartOptionView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + v = json.value; + if (v && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return 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: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + ChartOptionView.__super__.render.apply(this, arguments); + if (this.isCollapsed) { + this.$el.addClass('collapsed'); + } + return 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: function(state){ + state == null && (state = true); + state = !!state; + this.isCollapsed = this.$el.hasClass('collapsed'); + if (state === this.isCollapsed) { + return this; + } + if (state) { + this.$el.addClass('collapsed'); + } else { + this.$el.removeClass('collapsed'); + } + this.isCollapsed = state; + this.trigger('change:collapse', this, this.isCollapsed); + return true; + } + /** + * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event. + * @returns {this} + */, + toggleCollapsed: function(){ + this.collapse(!this.$el.hasClass('collapsed')); + return 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: function(evt){ + var target; + target = $(evt.target); + if (this.$el.hasClass('collapsed') && !target.hasClass('close')) { + return this.toggleCollapsed(); + } + } + /** + * Propagate user input changes to the model, and upward to the parent view. + */, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + console.log(this + ".onChange( " + current + " -> " + val + " )"); + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this.model, this); + } +}); +/** + * @class View for configuring a chart type. + */ +ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend({ + __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(){ + function ChartOptionScaffold(){ + return BaseView.apply(this, arguments); + } + return ChartOptionScaffold; + }()), + initialize: function(){ + var CollectionType; + this.render = _.debounce(this.render.bind(this), DEBOUNCE_RENDER); + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + ChartOptionScaffold.__super__.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + this.collection.on('reset', this.onReset, this); + return this.on('render', this.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: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + this.addSubview(view = new SubviewType({ + model: field + })).on('change', this.onChange.bind(this, field)).on('change:collapse', this.render, this); + this.render(); + return 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: function(){ + _.invoke(this.subviews, 'collapse', true); + return false; + } + /** + * Expand all collapsed subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */, + expandAll: function(){ + _.invoke(this.subviews, 'collapse', false); + return false; + } + /** + * Reflow Isotope post-`render()`. + */, + onRender: function(){ + if (!this.$el.is(':visible')) { + return; + } + return this.$('.isotope').isotope({ + itemSelector: '.chart-option.field', + layoutMode: 'masonry', + masonry: { + columnWidth: 10 + }, + filter: this.getOptionsFilter(), + sortBy: 'category', + getSortData: { + category: function($el){ + return $el.data('model').getCategory(); + } + } + }); + } + /** + * @returns {String} Selector representing the selected set of Option filters. + */, + getOptionsFilter: function(){ + var data, sel; + data = this.$('.options-filter-button.active').toArray().map(function(it){ + return $(it).data(); + }); + sel = data.reduce(function(sel, d){ + var that; + return sel += (that = d.filter) ? that : ''; + }, ':not(.ignore)'); + return sel; + } + /* * * * Events * * * */ + /** + * Propagate change events from fields as if they were attribute changes. + * Note: `field` is bound to the handler + */, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + }, + onReset: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return _.defer(this.render); + }, + onFilterOptions: function(evt){ + evt.preventDefault(); + return _.defer(this.render); + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return ChartOptionScaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); \ No newline at end of file diff --git a/lib/chart/option/chart-option-view.mod.js b/lib/chart/option/chart-option-view.mod.js new file mode 100644 index 0000000..8bf441a --- /dev/null +++ b/lib/chart/option/chart-option-view.mod.js @@ -0,0 +1,260 @@ +require.define('/node_modules/kraken/chart/option/chart-option-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseView, ChartOption, ChartOptionList, DEBOUNCE_RENDER, ChartOptionView, ChartOptionScaffold, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/chart/option/chart-option-model'), ChartOption = _ref.ChartOption, ChartOptionList = _ref.ChartOptionList; +DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100; +/** + * @class View for a single configurable option in a chart type. + */ +ChartOptionView = exports.ChartOptionView = BaseView.extend({ + 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(){ + function ChartOptionView(){ + return BaseView.apply(this, arguments); + } + return ChartOptionView; + }()), + initialize: function(){ + ChartOptionView.__super__.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + } + /* * * * Rendering * * * */, + toTemplateLocals: function(){ + var json, v; + json = ChartOptionView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + v = json.value; + if (v && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return 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: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + ChartOptionView.__super__.render.apply(this, arguments); + if (this.isCollapsed) { + this.$el.addClass('collapsed'); + } + return 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: function(state){ + state == null && (state = true); + state = !!state; + this.isCollapsed = this.$el.hasClass('collapsed'); + if (state === this.isCollapsed) { + return this; + } + if (state) { + this.$el.addClass('collapsed'); + } else { + this.$el.removeClass('collapsed'); + } + this.isCollapsed = state; + this.trigger('change:collapse', this, this.isCollapsed); + return true; + } + /** + * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event. + * @returns {this} + */, + toggleCollapsed: function(){ + this.collapse(!this.$el.hasClass('collapsed')); + return 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: function(evt){ + var target; + target = $(evt.target); + if (this.$el.hasClass('collapsed') && !target.hasClass('close')) { + return this.toggleCollapsed(); + } + } + /** + * Propagate user input changes to the model, and upward to the parent view. + */, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + console.log(this + ".onChange( " + current + " -> " + val + " )"); + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this.model, this); + } +}); +/** + * @class View for configuring a chart type. + */ +ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend({ + __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(){ + function ChartOptionScaffold(){ + return BaseView.apply(this, arguments); + } + return ChartOptionScaffold; + }()), + initialize: function(){ + var CollectionType; + this.render = _.debounce(this.render.bind(this), DEBOUNCE_RENDER); + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + ChartOptionScaffold.__super__.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + this.collection.on('reset', this.onReset, this); + return this.on('render', this.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: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + this.addSubview(view = new SubviewType({ + model: field + })).on('change', this.onChange.bind(this, field)).on('change:collapse', this.render, this); + this.render(); + return 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: function(){ + _.invoke(this.subviews, 'collapse', true); + return false; + } + /** + * Expand all collapsed subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */, + expandAll: function(){ + _.invoke(this.subviews, 'collapse', false); + return false; + } + /** + * Reflow Isotope post-`render()`. + */, + onRender: function(){ + if (!this.$el.is(':visible')) { + return; + } + return this.$('.isotope').isotope({ + itemSelector: '.chart-option.field', + layoutMode: 'masonry', + masonry: { + columnWidth: 10 + }, + filter: this.getOptionsFilter(), + sortBy: 'category', + getSortData: { + category: function($el){ + return $el.data('model').getCategory(); + } + } + }); + } + /** + * @returns {String} Selector representing the selected set of Option filters. + */, + getOptionsFilter: function(){ + var data, sel; + data = this.$('.options-filter-button.active').toArray().map(function(it){ + return $(it).data(); + }); + sel = data.reduce(function(sel, d){ + var that; + return sel += (that = d.filter) ? that : ''; + }, ':not(.ignore)'); + return sel; + } + /* * * * Events * * * */ + /** + * Propagate change events from fields as if they were attribute changes. + * Note: `field` is bound to the handler + */, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + }, + onReset: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return _.defer(this.render); + }, + onFilterOptions: function(evt){ + evt.preventDefault(); + return _.defer(this.render); + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return ChartOptionScaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); + +}); diff --git a/lib/chart/option/index.js b/lib/chart/option/index.js new file mode 100644 index 0000000..09d95b7 --- /dev/null +++ b/lib/chart/option/index.js @@ -0,0 +1,9 @@ +var model, view; +model = require('kraken/chart/option/chart-option-model'); +view = require('kraken/chart/option/chart-option-view'); +__import(__import(exports, model), view); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/option/index.mod.js b/lib/chart/option/index.mod.js new file mode 100644 index 0000000..15c866d --- /dev/null +++ b/lib/chart/option/index.mod.js @@ -0,0 +1,13 @@ +require.define('/node_modules/kraken/chart/option/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var model, view; +model = require('kraken/chart/option/chart-option-model'); +view = require('kraken/chart/option/chart-option-view'); +__import(__import(exports, model), view); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/type/d3-chart.js b/lib/chart/type/d3-chart.js new file mode 100644 index 0000000..adce99c --- /dev/null +++ b/lib/chart/type/d3-chart.js @@ -0,0 +1,92 @@ +var d3, ColorBrewer, op, ChartType, D3ChartElement, root, D3ChartType, _ref, _; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ChartType = require('kraken/chart/chart-type').ChartType; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +root = function(){ + return this; +}(); +exports.D3ChartType = D3ChartType = (function(superclass){ + D3ChartType.displayName = 'D3ChartType'; + var prototype = __extend(D3ChartType, superclass).prototype, constructor = D3ChartType; + prototype.__bind__ = ['determineSize']; + prototype.SPEC_URL = '/schema/d3/d3-chart.json'; + prototype.typeName = 'd3-chart'; + ChartType.register(D3ChartType); + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport', + legend: '.graph-legend' + }; + function D3ChartType(){ + superclass.apply(this, arguments); + } + prototype.getData = function(){ + return this.model.dataset.getColumns(); + }; + prototype.transform = function(){ + var dataset, options; + dataset = this.model.dataset; + options = __import(this.model.getOptions(), this.determineSize()); + __import(options, { + colors: dataset.getColors(), + labels: dataset.getLabels() + }); + return options; + }; + prototype.renderChart = function(data, viewport, options, lastChart){ + var margin, width, height, xScale, yScale, dates, cols, allValues, svg, enterFrame, frame, xAxis, metrics; + 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); + allValues = d3.merge(cols); + xScale.domain(d3.extent(dates)).range([0, width]); + yScale.domain(d3.extent(allValues)).range([height, 0]); + svg = d3.select(viewport[0]).selectAll("svg").remove(); + svg = d3.select(viewport[0]).selectAll("svg").data([cols]); + enterFrame = svg.enter().append("svg").append("g").attr("class", "frame"); + 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); + 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(this.model.dataset.metrics.models); + metrics.enter().append("g").attr("class", function(d){ + return "g metric line " + d.get('label'); + }).each(function(d){ + var chartElement, chEl; + chartElement = d.get("chartElement"); + chartElement == null && (chartElement = 'd3-line'); + chEl = D3ChartElement.create(chartElement); + return chEl.renderChartElement(d, frame, xScale, yScale); + }); + metrics.exit().remove(); + return svg; + }; + return D3ChartType; +}(ChartType)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/type/d3-chart.mod.js b/lib/chart/type/d3-chart.mod.js new file mode 100644 index 0000000..f6817e0 --- /dev/null +++ b/lib/chart/type/d3-chart.mod.js @@ -0,0 +1,96 @@ +require.define('/node_modules/kraken/chart/type/d3-chart.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3, ColorBrewer, op, ChartType, D3ChartElement, root, D3ChartType, _ref, _; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ChartType = require('kraken/chart/chart-type').ChartType; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +root = function(){ + return this; +}(); +exports.D3ChartType = D3ChartType = (function(superclass){ + D3ChartType.displayName = 'D3ChartType'; + var prototype = __extend(D3ChartType, superclass).prototype, constructor = D3ChartType; + prototype.__bind__ = ['determineSize']; + prototype.SPEC_URL = '/schema/d3/d3-chart.json'; + prototype.typeName = 'd3-chart'; + ChartType.register(D3ChartType); + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport', + legend: '.graph-legend' + }; + function D3ChartType(){ + superclass.apply(this, arguments); + } + prototype.getData = function(){ + return this.model.dataset.getColumns(); + }; + prototype.transform = function(){ + var dataset, options; + dataset = this.model.dataset; + options = __import(this.model.getOptions(), this.determineSize()); + __import(options, { + colors: dataset.getColors(), + labels: dataset.getLabels() + }); + return options; + }; + prototype.renderChart = function(data, viewport, options, lastChart){ + var margin, width, height, xScale, yScale, dates, cols, allValues, svg, enterFrame, frame, xAxis, metrics; + 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); + allValues = d3.merge(cols); + xScale.domain(d3.extent(dates)).range([0, width]); + yScale.domain(d3.extent(allValues)).range([height, 0]); + svg = d3.select(viewport[0]).selectAll("svg").remove(); + svg = d3.select(viewport[0]).selectAll("svg").data([cols]); + enterFrame = svg.enter().append("svg").append("g").attr("class", "frame"); + 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); + 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(this.model.dataset.metrics.models); + metrics.enter().append("g").attr("class", function(d){ + return "g metric line " + d.get('label'); + }).each(function(d){ + var chartElement, chEl; + chartElement = d.get("chartElement"); + chartElement == null && (chartElement = 'd3-line'); + chEl = D3ChartElement.create(chartElement); + return chEl.renderChartElement(d, frame, xScale, yScale); + }); + metrics.exit().remove(); + return svg; + }; + return D3ChartType; +}(ChartType)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/type/d3/d3-bar-element.js b/lib/chart/type/d3/d3-bar-element.js new file mode 100644 index 0000000..b1248b2 --- /dev/null +++ b/lib/chart/type/d3/d3-bar-element.js @@ -0,0 +1,51 @@ +var d3, op, D3ChartElement, root, BarChartType, _ref, _, _fmt; +d3 = require('d3'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +_fmt = require('kraken/util/formatters'); +root = function(){ + return this; +}(); +exports.BarChartType = BarChartType = (function(superclass){ + BarChartType.displayName = 'BarChartType'; + var prototype = __extend(BarChartType, superclass).prototype, constructor = BarChartType; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-bar.json'; + prototype.chartElement = 'd3-bar'; + D3ChartElement.register(BarChartType); + function BarChartType(){ + superclass.apply(this, arguments); + } + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + var X, Y, metricBars, data, barWidth, barHeight, chT; + X = function(d, i){ + return xScale(d[0]); + }; + Y = function(d, i){ + return yScale(d[1]); + }; + metricBars = root.metricBars = svgEl.append("g").attr("class", "metric bars " + metric.get('label')); + data = d3.zip(metric.getDateColumn(), metric.getData()); + barWidth = svgEl.attr('width') / data.length; + barHeight = function(d){ + return svgEl.attr('height') - Y(d); + }; + metricBars.selectAll("bar").data(data).enter().append("rect").attr("class", function(d, i){ + return "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); + chT = this; + metricBars.selectAll(".metric.bar").on("mouseover", function(d, i){ + return 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", function(d, i){ + return svgEl.selectAll(".mf").transition().duration(300).text("BUMMER!!!").style("font-size", "0px").remove(); + }); + return svgEl; + }; + return BarChartType; +}(D3ChartElement)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/chart/type/d3/d3-bar-element.mod.js b/lib/chart/type/d3/d3-bar-element.mod.js new file mode 100644 index 0000000..cd8d26a --- /dev/null +++ b/lib/chart/type/d3/d3-bar-element.mod.js @@ -0,0 +1,55 @@ +require.define('/node_modules/kraken/chart/type/d3/d3-bar-element.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3, op, D3ChartElement, root, BarChartType, _ref, _, _fmt; +d3 = require('d3'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +_fmt = require('kraken/util/formatters'); +root = function(){ + return this; +}(); +exports.BarChartType = BarChartType = (function(superclass){ + BarChartType.displayName = 'BarChartType'; + var prototype = __extend(BarChartType, superclass).prototype, constructor = BarChartType; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-bar.json'; + prototype.chartElement = 'd3-bar'; + D3ChartElement.register(BarChartType); + function BarChartType(){ + superclass.apply(this, arguments); + } + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + var X, Y, metricBars, data, barWidth, barHeight, chT; + X = function(d, i){ + return xScale(d[0]); + }; + Y = function(d, i){ + return yScale(d[1]); + }; + metricBars = root.metricBars = svgEl.append("g").attr("class", "metric bars " + metric.get('label')); + data = d3.zip(metric.getDateColumn(), metric.getData()); + barWidth = svgEl.attr('width') / data.length; + barHeight = function(d){ + return svgEl.attr('height') - Y(d); + }; + metricBars.selectAll("bar").data(data).enter().append("rect").attr("class", function(d, i){ + return "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); + chT = this; + metricBars.selectAll(".metric.bar").on("mouseover", function(d, i){ + return 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", function(d, i){ + return svgEl.selectAll(".mf").transition().duration(300).text("BUMMER!!!").style("font-size", "0px").remove(); + }); + return svgEl; + }; + return BarChartType; +}(D3ChartElement)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/chart/type/d3/d3-chart-element.js b/lib/chart/type/d3/d3-chart-element.js new file mode 100644 index 0000000..d61b091 --- /dev/null +++ b/lib/chart/type/d3/d3-chart-element.js @@ -0,0 +1,91 @@ +var d3, ColorBrewer, op, ReadyEmitter, root, KNOWN_CHART_ELEMENTS, D3ChartElement, _ref, _, __slice = [].slice; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +root = function(){ + return this; +}(); +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_ELEMENTS = exports.KNOWN_CHART_ELEMENTS = {}; +exports.D3ChartElement = D3ChartElement = (function(superclass){ + D3ChartElement.displayName = 'D3ChartElement'; + var prototype = __extend(D3ChartElement, superclass).prototype, constructor = D3ChartElement; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-chart.json'; + /** + * Register a new d3 element + */; + D3ChartElement.register = function(Subclass){ + return KNOWN_CHART_ELEMENTS[Subclass.prototype.chartElement] = Subclass; + }; + /** + * Look up a `charttype` by `typeName`. + */ + D3ChartElement.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartElement'); + } + return KNOWN_CHART_ELEMENTS[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {D3ChartElement} + */ + D3ChartElement.create = function(name){ + var Type; + if (!(Type = this.lookup(name))) { + return null; + } + return new Type; + }; + function D3ChartElement(){ + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + superclass.apply(this, arguments); + } + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + prototype.loadSpec = function(){ + var proto, _this = this; + if (this.ready) { + return this; + } + proto = this.constructor.prototype; + jQuery.ajax({ + url: this.SPEC_URL, + dataType: 'json', + success: function(spec){ + proto.spec = spec; + proto.options_ordered = spec; + proto.options = _.synthesize(spec, function(it){ + return [it.name, it]; + }); + proto.ready = true; + return _this.triggerReady(); + }, + error: function(it){ + return console.error("Error loading " + _this.typeName + " spec! " + it); + } + }); + return this; + }; + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + return svgEl; + }; + return D3ChartElement; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/chart/type/d3/d3-chart-element.mod.js b/lib/chart/type/d3/d3-chart-element.mod.js new file mode 100644 index 0000000..4a9e29e --- /dev/null +++ b/lib/chart/type/d3/d3-chart-element.mod.js @@ -0,0 +1,95 @@ +require.define('/node_modules/kraken/chart/type/d3/d3-chart-element.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3, ColorBrewer, op, ReadyEmitter, root, KNOWN_CHART_ELEMENTS, D3ChartElement, _ref, _, __slice = [].slice; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +root = function(){ + return this; +}(); +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_ELEMENTS = exports.KNOWN_CHART_ELEMENTS = {}; +exports.D3ChartElement = D3ChartElement = (function(superclass){ + D3ChartElement.displayName = 'D3ChartElement'; + var prototype = __extend(D3ChartElement, superclass).prototype, constructor = D3ChartElement; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-chart.json'; + /** + * Register a new d3 element + */; + D3ChartElement.register = function(Subclass){ + return KNOWN_CHART_ELEMENTS[Subclass.prototype.chartElement] = Subclass; + }; + /** + * Look up a `charttype` by `typeName`. + */ + D3ChartElement.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartElement'); + } + return KNOWN_CHART_ELEMENTS[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {D3ChartElement} + */ + D3ChartElement.create = function(name){ + var Type; + if (!(Type = this.lookup(name))) { + return null; + } + return new Type; + }; + function D3ChartElement(){ + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + superclass.apply(this, arguments); + } + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + prototype.loadSpec = function(){ + var proto, _this = this; + if (this.ready) { + return this; + } + proto = this.constructor.prototype; + jQuery.ajax({ + url: this.SPEC_URL, + dataType: 'json', + success: function(spec){ + proto.spec = spec; + proto.options_ordered = spec; + proto.options = _.synthesize(spec, function(it){ + return [it.name, it]; + }); + proto.ready = true; + return _this.triggerReady(); + }, + error: function(it){ + return console.error("Error loading " + _this.typeName + " spec! " + it); + } + }); + return this; + }; + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + return svgEl; + }; + return D3ChartElement; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/chart/type/d3/d3-line-element.js b/lib/chart/type/d3/d3-line-element.js new file mode 100644 index 0000000..c6c5867 --- /dev/null +++ b/lib/chart/type/d3/d3-line-element.js @@ -0,0 +1,59 @@ +var d3, ColorBrewer, op, D3ChartElement, root, LineChartElement, _ref, _, _fmt; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +_fmt = require('kraken/util/formatters'); +root = function(){ + return this; +}(); +exports.LineChartElement = LineChartElement = (function(superclass){ + LineChartElement.displayName = 'LineChartElement'; + var prototype = __extend(LineChartElement, superclass).prototype, constructor = LineChartElement; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-line.json'; + prototype.chartElement = 'd3-line'; + D3ChartElement.register(LineChartElement); + function LineChartElement(){ + superclass.apply(this, arguments); + } + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + var X, Y, line, metricLine, data, lens, gLens, gInner; + X = function(d, i){ + return xScale(d[0]); + }; + Y = function(d, i){ + return yScale(d[1]); + }; + line = d3.svg.line().x(X).y(Y); + 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", function(d, i){ + return "metric line segment " + i; + }).style("stroke", metric.getColor('color')); + 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("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"); + metricLine.selectAll(".line.segment").on("mouseover", function(d, i){ + var color, r, g, b, lineX, lineY, lens, _ref; + _ref = color = d3.rgb(metric.getColor('color')), r = _ref.r, g = _ref.g, b = _ref.b; + 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)"); + return lens.select("text").text(function(){ + return _fmt.numberFormatter(d[0][1]).toString(); + }); + }); + return svgEl; + }; + return LineChartElement; +}(D3ChartElement)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/chart/type/d3/d3-line-element.mod.js b/lib/chart/type/d3/d3-line-element.mod.js new file mode 100644 index 0000000..c0458f7 --- /dev/null +++ b/lib/chart/type/d3/d3-line-element.mod.js @@ -0,0 +1,63 @@ +require.define('/node_modules/kraken/chart/type/d3/d3-line-element.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3, ColorBrewer, op, D3ChartElement, root, LineChartElement, _ref, _, _fmt; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +_fmt = require('kraken/util/formatters'); +root = function(){ + return this; +}(); +exports.LineChartElement = LineChartElement = (function(superclass){ + LineChartElement.displayName = 'LineChartElement'; + var prototype = __extend(LineChartElement, superclass).prototype, constructor = LineChartElement; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-line.json'; + prototype.chartElement = 'd3-line'; + D3ChartElement.register(LineChartElement); + function LineChartElement(){ + superclass.apply(this, arguments); + } + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + var X, Y, line, metricLine, data, lens, gLens, gInner; + X = function(d, i){ + return xScale(d[0]); + }; + Y = function(d, i){ + return yScale(d[1]); + }; + line = d3.svg.line().x(X).y(Y); + 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", function(d, i){ + return "metric line segment " + i; + }).style("stroke", metric.getColor('color')); + 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("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"); + metricLine.selectAll(".line.segment").on("mouseover", function(d, i){ + var color, r, g, b, lineX, lineY, lens, _ref; + _ref = color = d3.rgb(metric.getColor('color')), r = _ref.r, g = _ref.g, b = _ref.b; + 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)"); + return lens.select("text").text(function(){ + return _fmt.numberFormatter(d[0][1]).toString(); + }); + }); + return svgEl; + }; + return LineChartElement; +}(D3ChartElement)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/chart/type/d3/index.js b/lib/chart/type/d3/index.js new file mode 100644 index 0000000..c1193b7 --- /dev/null +++ b/lib/chart/type/d3/index.js @@ -0,0 +1,10 @@ +var d3chart, line, bar; +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'); +__import(__import(__import(exports, line), bar), d3chart); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/type/d3/index.mod.js b/lib/chart/type/d3/index.mod.js new file mode 100644 index 0000000..9c2a284 --- /dev/null +++ b/lib/chart/type/d3/index.mod.js @@ -0,0 +1,14 @@ +require.define('/node_modules/kraken/chart/type/d3/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3chart, line, bar; +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'); +__import(__import(__import(exports, line), bar), d3chart); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/type/dygraphs.js b/lib/chart/type/dygraphs.js new file mode 100644 index 0000000..59865da --- /dev/null +++ b/lib/chart/type/dygraphs.js @@ -0,0 +1,127 @@ +var ChartType, DygraphsChartType, _; +_ = require('kraken/util/underscore'); +ChartType = require('kraken/chart/chart-type').ChartType; +exports.DygraphsChartType = DygraphsChartType = (function(superclass){ + DygraphsChartType.displayName = 'DygraphsChartType'; + var prototype = __extend(DygraphsChartType, superclass).prototype, constructor = DygraphsChartType; + prototype.__bind__ = ['dygNumberFormatter', 'dygNumberFormatterHTML']; + prototype.SPEC_URL = '/schema/dygraph.json'; + prototype.typeName = 'dygraphs'; + ChartType.register(DygraphsChartType); + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport', + legend: '.graph-legend' + }; + function DygraphsChartType(){ + superclass.apply(this, arguments); + } + prototype.makeAxisFormatter = function(fmttr){ + return function(n, granularity, opts, g){ + return fmttr(n, opts, g); + }; + }; + prototype.dygAxisDateFormatter = function(n, granularity, opts, g){ + return moment(n).format('MM/YYYY'); + }; + prototype.dygDateFormatter = function(n, opts, g){ + return moment(n).format('DD MMM YYYY'); + }; + prototype.dygNumberFormatter = function(n, opts, g){ + var that, digits, whole, fraction, suffix, _ref; + digits = (that = typeof opts('digitsAfterDecimal') === 'number') ? that : 2; + _ref = this.numberFormatter(n, digits), whole = _ref.whole, fraction = _ref.fraction, suffix = _ref.suffix; + return whole + "" + fraction + suffix; + }; + prototype.dygNumberFormatterHTML = function(n, opts, g){ + var that, digits, whole, fraction, suffix, _ref; + digits = (that = typeof opts('digitsAfterDecimal') === 'number') ? that : 2; + _ref = this.numberFormatter(n, digits), whole = _ref.whole, fraction = _ref.fraction, suffix = _ref.suffix; + return "" + whole + "" + fraction + "" + suffix + ""; + }; + /** + * Determines chart viewport size. + * @return { width, height } + */ + prototype.determineSize = function(){ + var width, modelW, height, modelH, viewport, legend, vpWidth, legendW; + modelW = width = this.model.get('width'); + modelH = height = this.model.get('height'); + if (!(this.view.ready && width && height)) { + return { + width: width, + height: height + }; + } + viewport = this.getElementsForRole('viewport'); + legend = this.getElementsForRole('legend'); + if (width === 'auto') { + delete viewport.prop('style').width; + vpWidth = viewport.innerWidth() || 300; + legendW = legend.outerWidth() || 228; + width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW); + } + width == null && (width = modelW); + if (height === 'auto') { + delete viewport.prop('style').height; + height = viewport.innerHeight() || 320; + } + height == null && (height = modelH); + return { + width: width, + height: height + }; + }; + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * @returns {Object} The derived chart options. + */ + prototype.transform = function(){ + var dataset, options; + dataset = this.model.dataset; + options = __import(this.view.chartOptions(), this.determineSize()); + return __import(options, { + colors: dataset.getColors(), + labels: dataset.getLabels(), + labelsDiv: this.getElementsForRole('legend')[0], + valueFormatter: this.dygNumberFormatterHTML, + axes: { + x: { + axisLabelFormatter: this.dygAxisDateFormatter, + valueFormatter: this.dygDateFormatter + }, + y: { + axisLabelFormatter: this.makeAxisFormatter(this.dygNumberFormatter), + valueFormatter: this.dygNumberFormatterHTML + } + } + }); + }; + /** + * @returns {Dygraph} The Dygraph chart object. + */ + prototype.renderChart = function(data, viewport, options, lastChart){ + this.resizeViewport(); + if (lastChart != null) { + lastChart.destroy(); + } + return new Dygraph(viewport[0], data, options); + }; + return DygraphsChartType; +}(ChartType)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/type/dygraphs.mod.js b/lib/chart/type/dygraphs.mod.js new file mode 100644 index 0000000..68e7d1f --- /dev/null +++ b/lib/chart/type/dygraphs.mod.js @@ -0,0 +1,131 @@ +require.define('/node_modules/kraken/chart/type/dygraphs.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var ChartType, DygraphsChartType, _; +_ = require('kraken/util/underscore'); +ChartType = require('kraken/chart/chart-type').ChartType; +exports.DygraphsChartType = DygraphsChartType = (function(superclass){ + DygraphsChartType.displayName = 'DygraphsChartType'; + var prototype = __extend(DygraphsChartType, superclass).prototype, constructor = DygraphsChartType; + prototype.__bind__ = ['dygNumberFormatter', 'dygNumberFormatterHTML']; + prototype.SPEC_URL = '/schema/dygraph.json'; + prototype.typeName = 'dygraphs'; + ChartType.register(DygraphsChartType); + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport', + legend: '.graph-legend' + }; + function DygraphsChartType(){ + superclass.apply(this, arguments); + } + prototype.makeAxisFormatter = function(fmttr){ + return function(n, granularity, opts, g){ + return fmttr(n, opts, g); + }; + }; + prototype.dygAxisDateFormatter = function(n, granularity, opts, g){ + return moment(n).format('MM/YYYY'); + }; + prototype.dygDateFormatter = function(n, opts, g){ + return moment(n).format('DD MMM YYYY'); + }; + prototype.dygNumberFormatter = function(n, opts, g){ + var that, digits, whole, fraction, suffix, _ref; + digits = (that = typeof opts('digitsAfterDecimal') === 'number') ? that : 2; + _ref = this.numberFormatter(n, digits), whole = _ref.whole, fraction = _ref.fraction, suffix = _ref.suffix; + return whole + "" + fraction + suffix; + }; + prototype.dygNumberFormatterHTML = function(n, opts, g){ + var that, digits, whole, fraction, suffix, _ref; + digits = (that = typeof opts('digitsAfterDecimal') === 'number') ? that : 2; + _ref = this.numberFormatter(n, digits), whole = _ref.whole, fraction = _ref.fraction, suffix = _ref.suffix; + return "" + whole + "" + fraction + "" + suffix + ""; + }; + /** + * Determines chart viewport size. + * @return { width, height } + */ + prototype.determineSize = function(){ + var width, modelW, height, modelH, viewport, legend, vpWidth, legendW; + modelW = width = this.model.get('width'); + modelH = height = this.model.get('height'); + if (!(this.view.ready && width && height)) { + return { + width: width, + height: height + }; + } + viewport = this.getElementsForRole('viewport'); + legend = this.getElementsForRole('legend'); + if (width === 'auto') { + delete viewport.prop('style').width; + vpWidth = viewport.innerWidth() || 300; + legendW = legend.outerWidth() || 228; + width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW); + } + width == null && (width = modelW); + if (height === 'auto') { + delete viewport.prop('style').height; + height = viewport.innerHeight() || 320; + } + height == null && (height = modelH); + return { + width: width, + height: height + }; + }; + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * @returns {Object} The derived chart options. + */ + prototype.transform = function(){ + var dataset, options; + dataset = this.model.dataset; + options = __import(this.view.chartOptions(), this.determineSize()); + return __import(options, { + colors: dataset.getColors(), + labels: dataset.getLabels(), + labelsDiv: this.getElementsForRole('legend')[0], + valueFormatter: this.dygNumberFormatterHTML, + axes: { + x: { + axisLabelFormatter: this.dygAxisDateFormatter, + valueFormatter: this.dygDateFormatter + }, + y: { + axisLabelFormatter: this.makeAxisFormatter(this.dygNumberFormatter), + valueFormatter: this.dygNumberFormatterHTML + } + } + }); + }; + /** + * @returns {Dygraph} The Dygraph chart object. + */ + prototype.renderChart = function(data, viewport, options, lastChart){ + this.resizeViewport(); + if (lastChart != null) { + lastChart.destroy(); + } + return new Dygraph(viewport[0], data, options); + }; + return DygraphsChartType; +}(ChartType)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/type/index.js b/lib/chart/type/index.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/chart/type/index.mod.js b/lib/chart/type/index.mod.js new file mode 100644 index 0000000..c1940bf --- /dev/null +++ b/lib/chart/type/index.mod.js @@ -0,0 +1,5 @@ +require.define('/node_modules/kraken/chart/type/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + + + +}); diff --git a/lib/dashboard/dashboard-model.js b/lib/dashboard/dashboard-model.js new file mode 100644 index 0000000..8e26e25 --- /dev/null +++ b/lib/dashboard/dashboard-model.js @@ -0,0 +1,87 @@ +var op, BaseModel, Graph, GraphList, Dashboard, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseModel = require('kraken/base').BaseModel; +_ref = require('kraken/graph/graph-model'), Graph = _ref.Graph, GraphList = _ref.GraphList; +/** + * @class + */ +Dashboard = exports.Dashboard = BaseModel.extend({ + urlRoot: '/dashboards', + graphs: null, + constructor: (function(){ + function Dashboard(){ + this.graphs = new GraphList; + return BaseModel.apply(this, arguments); + } + return Dashboard; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + }, + defaults: function(){ + return { + name: null, + tabs: [{ + name: "Main", + graph_ids: [] + }] + }; + }, + load: function(){ + var _this = this; + this.once('fetch-success', function(){ + return _this.getGraphs(); + }).loadModel(); + return this; + } + /** + * Look up a tab. + * + * @param {String|Number} tab Tab name or index. + * @returns {Tab} Tab object. + */, + getTab: function(tab){ + var tabs; + tabs = this.get('tabs'); + if (typeof tab === 'number') { + return tabs[tab]; + } + return _.find(tabs, function(it){ + return it.name === tab; + }); + }, + show: function(cb, obj){ + console.log('[show]'); + console.log(obj); + return cb(null, obj); + }, + pushAsync: function(cb, arr){ + return function(err, elem){ + arr.push(elem); + return cb(null); + }; + }, + getGraphs: function(){ + var graph_ids, _this = this; + console.log('[getGraphs]\tentering'); + graph_ids = _(this.tabs).chain().values().map(function(tab_obj){ + return tab_obj.graph_ids; + }).flatten().value(); + Seq(graph_ids).parMap_(function(next, graph_id){ + return next(null, [graph_id]); + }).parEach_(function(next, graph_id_arr){ + return Graph.lookup(graph_id_arr[0], _this.pushAsync(next, graph_id_arr)); + }).parMap_(function(next, tuple){ + var id, graph; + id = tuple[0], graph = tuple[1]; + return graph.once('ready', function(){ + return next.ok(tuple); + }); + }).unflatten().seq_(function(next, graph_tuples){ + _this.graphs.reset(_.pluck(graph_tuples, 1)); + console.log('[setter]\tcalling ready'); + return _this.triggerReady(); + }); + return this; + } +}); \ No newline at end of file diff --git a/lib/dashboard/dashboard-model.mod.js b/lib/dashboard/dashboard-model.mod.js new file mode 100644 index 0000000..7e61eb7 --- /dev/null +++ b/lib/dashboard/dashboard-model.mod.js @@ -0,0 +1,91 @@ +require.define('/node_modules/kraken/dashboard/dashboard-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseModel, Graph, GraphList, Dashboard, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseModel = require('kraken/base').BaseModel; +_ref = require('kraken/graph/graph-model'), Graph = _ref.Graph, GraphList = _ref.GraphList; +/** + * @class + */ +Dashboard = exports.Dashboard = BaseModel.extend({ + urlRoot: '/dashboards', + graphs: null, + constructor: (function(){ + function Dashboard(){ + this.graphs = new GraphList; + return BaseModel.apply(this, arguments); + } + return Dashboard; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + }, + defaults: function(){ + return { + name: null, + tabs: [{ + name: "Main", + graph_ids: [] + }] + }; + }, + load: function(){ + var _this = this; + this.once('fetch-success', function(){ + return _this.getGraphs(); + }).loadModel(); + return this; + } + /** + * Look up a tab. + * + * @param {String|Number} tab Tab name or index. + * @returns {Tab} Tab object. + */, + getTab: function(tab){ + var tabs; + tabs = this.get('tabs'); + if (typeof tab === 'number') { + return tabs[tab]; + } + return _.find(tabs, function(it){ + return it.name === tab; + }); + }, + show: function(cb, obj){ + console.log('[show]'); + console.log(obj); + return cb(null, obj); + }, + pushAsync: function(cb, arr){ + return function(err, elem){ + arr.push(elem); + return cb(null); + }; + }, + getGraphs: function(){ + var graph_ids, _this = this; + console.log('[getGraphs]\tentering'); + graph_ids = _(this.tabs).chain().values().map(function(tab_obj){ + return tab_obj.graph_ids; + }).flatten().value(); + Seq(graph_ids).parMap_(function(next, graph_id){ + return next(null, [graph_id]); + }).parEach_(function(next, graph_id_arr){ + return Graph.lookup(graph_id_arr[0], _this.pushAsync(next, graph_id_arr)); + }).parMap_(function(next, tuple){ + var id, graph; + id = tuple[0], graph = tuple[1]; + return graph.once('ready', function(){ + return next.ok(tuple); + }); + }).unflatten().seq_(function(next, graph_tuples){ + _this.graphs.reset(_.pluck(graph_tuples, 1)); + console.log('[setter]\tcalling ready'); + return _this.triggerReady(); + }); + return this; + } +}); + +}); diff --git a/lib/dashboard/dashboard-view.js b/lib/dashboard/dashboard-view.js new file mode 100644 index 0000000..37b4d79 --- /dev/null +++ b/lib/dashboard/dashboard-view.js @@ -0,0 +1,148 @@ +var Seq, op, BaseModel, BaseView, Graph, GraphList, GraphDisplayView, Dashboard, DashboardView, DashboardTabView, _ref, _; +Seq = require('seq'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseView = _ref.BaseView; +_ref = require('kraken/graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView; +Dashboard = require('kraken/dashboard/dashboard-model').Dashboard; +/** + * @class + */ +DashboardView = exports.DashboardView = BaseView.extend({ + __bind__: ['addTab'], + tagName: 'section', + className: 'dashboard', + template: require('kraken/template/dashboard/dashboard'), + events: { + 'click .graphs.tabbable .nav a': 'onTabClick', + 'shown .graphs.tabbable .nav a': 'render' + }, + graphs: null, + ready: false, + constructor: (function(){ + function DashboardView(options){ + options == null && (options = {}); + this.graphs = new GraphList; + return BaseView.apply(this, arguments); + } + return DashboardView; + }()), + initialize: function(){ + this.model || (this.model = new Dashboard); + DashboardView.__super__.initialize.apply(this, arguments); + return this.model.once('ready', this.load, this).load(); + }, + load: function(){ + var _this = this; + console.log(this + ".load! Model ready!", this.model); + return Seq(this.model.get('tabs')).seqEach_(this.addTab).seq(function(){ + console.log(_this + ".load! Done adding tabs!"); + return _this.triggerReady(); + }); + }, + addTab: function(nextTab, tab){ + var tabModel, tabView, tabId, graphs, _this = this; + tabModel = new BaseModel(tab); + tabView = this.addSubview(new DashboardTabView({ + model: tabModel + })); + tabId = tabView.getTabId(); + this.$("nav > ul.nav").append("
  • " + tab.name + "
  • "); + graphs = _(tab.graph_ids).map(function(graph_id){ + return _this.model.graphs.get(graph_id); + }); + Seq(graphs).parMap_(function(next, graph){ + _this.graphs.add(graph); + return next(null, new GraphDisplayView({ + model: graph + })); + }).parMap_(function(next, graphView){ + if (graphView.isAttached) { + return next.ok(); + } + tabView.addSubview(graphView); + return next.ok(); + }).seq(function(){ + console.log(_this + ".addTab: All graphs added!"); + return nextTab.ok(); + }); + return this; + }, + onTabShown: function(e){ + return this.render(); + }, + onTabClick: function(evt){ + return evt.preventDefault(); + } + /** + * 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: function(graph){ + var view; + if (typeof graph === 'string') { + graph = this.graphs.get(graph); + } else if (typeof graph === 'number') { + graph = this.graphs.at(graph); + } + if (!(graph instanceof Graph)) { + console.error(this + ".scrollToGraph() Unknown graph " + graph + "!"); + return this; + } + if (!(view = _.find(this.subviews, function(it){ + return it.model === graph; + }))) { + return this; + } + if (view.$el.is(':visible')) { + $('body').scrollTop(view.$el.offset().top); + } + return this; + }, + findClosestGraph: function(scroll){ + var views; + scroll || (scroll = $('body').scrollTop()); + views = this.subviews.filter(function(it){ + return it.$el.is(':visible'); + }).map(function(it){ + return [it.$el.offset().top, it]; + }).filter(function(it){ + return it[0] >= scroll; + }).sort(function(a, b){ + return op.cmp(a[0], b[0]); + }); + if (views.length) { + return views[0][1]; + } + } +}); +/** + * @class + * @extends BaseView + */ +DashboardTabView = exports.DashboardTabView = BaseView.extend({ + __bind__: [], + className: 'tab-pane', + tag: 'div', + template: require('kraken/template/dashboard/dashboard-tab'), + constructor: (function(){ + function DashboardTabView(){ + return BaseView.apply(this, arguments); + } + return DashboardTabView; + }()), + initialize: function(){ + return BaseView.prototype.initialize.apply(this, arguments); + }, + getTabId: function(){ + return _.underscored(this.model.get('name')).toLowerCase() + '-graphs-tab'; + }, + toTemplateLocals: function(){ + var json, tab_name; + json = DashboardTabView.__super__.toTemplateLocals.apply(this, arguments); + tab_name = _.underscored(this.model.get('name')).toLowerCase(); + return json.tab_cls = tab_name + "-graphs-pane", json.tab_id = tab_name + "-graphs-tab", json; + } +}); \ No newline at end of file diff --git a/lib/dashboard/dashboard-view.mod.js b/lib/dashboard/dashboard-view.mod.js new file mode 100644 index 0000000..cc0228a --- /dev/null +++ b/lib/dashboard/dashboard-view.mod.js @@ -0,0 +1,152 @@ +require.define('/node_modules/kraken/dashboard/dashboard-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, op, BaseModel, BaseView, Graph, GraphList, GraphDisplayView, Dashboard, DashboardView, DashboardTabView, _ref, _; +Seq = require('seq'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseView = _ref.BaseView; +_ref = require('kraken/graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView; +Dashboard = require('kraken/dashboard/dashboard-model').Dashboard; +/** + * @class + */ +DashboardView = exports.DashboardView = BaseView.extend({ + __bind__: ['addTab'], + tagName: 'section', + className: 'dashboard', + template: require('kraken/template/dashboard/dashboard'), + events: { + 'click .graphs.tabbable .nav a': 'onTabClick', + 'shown .graphs.tabbable .nav a': 'render' + }, + graphs: null, + ready: false, + constructor: (function(){ + function DashboardView(options){ + options == null && (options = {}); + this.graphs = new GraphList; + return BaseView.apply(this, arguments); + } + return DashboardView; + }()), + initialize: function(){ + this.model || (this.model = new Dashboard); + DashboardView.__super__.initialize.apply(this, arguments); + return this.model.once('ready', this.load, this).load(); + }, + load: function(){ + var _this = this; + console.log(this + ".load! Model ready!", this.model); + return Seq(this.model.get('tabs')).seqEach_(this.addTab).seq(function(){ + console.log(_this + ".load! Done adding tabs!"); + return _this.triggerReady(); + }); + }, + addTab: function(nextTab, tab){ + var tabModel, tabView, tabId, graphs, _this = this; + tabModel = new BaseModel(tab); + tabView = this.addSubview(new DashboardTabView({ + model: tabModel + })); + tabId = tabView.getTabId(); + this.$("nav > ul.nav").append("
  • " + tab.name + "
  • "); + graphs = _(tab.graph_ids).map(function(graph_id){ + return _this.model.graphs.get(graph_id); + }); + Seq(graphs).parMap_(function(next, graph){ + _this.graphs.add(graph); + return next(null, new GraphDisplayView({ + model: graph + })); + }).parMap_(function(next, graphView){ + if (graphView.isAttached) { + return next.ok(); + } + tabView.addSubview(graphView); + return next.ok(); + }).seq(function(){ + console.log(_this + ".addTab: All graphs added!"); + return nextTab.ok(); + }); + return this; + }, + onTabShown: function(e){ + return this.render(); + }, + onTabClick: function(evt){ + return evt.preventDefault(); + } + /** + * 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: function(graph){ + var view; + if (typeof graph === 'string') { + graph = this.graphs.get(graph); + } else if (typeof graph === 'number') { + graph = this.graphs.at(graph); + } + if (!(graph instanceof Graph)) { + console.error(this + ".scrollToGraph() Unknown graph " + graph + "!"); + return this; + } + if (!(view = _.find(this.subviews, function(it){ + return it.model === graph; + }))) { + return this; + } + if (view.$el.is(':visible')) { + $('body').scrollTop(view.$el.offset().top); + } + return this; + }, + findClosestGraph: function(scroll){ + var views; + scroll || (scroll = $('body').scrollTop()); + views = this.subviews.filter(function(it){ + return it.$el.is(':visible'); + }).map(function(it){ + return [it.$el.offset().top, it]; + }).filter(function(it){ + return it[0] >= scroll; + }).sort(function(a, b){ + return op.cmp(a[0], b[0]); + }); + if (views.length) { + return views[0][1]; + } + } +}); +/** + * @class + * @extends BaseView + */ +DashboardTabView = exports.DashboardTabView = BaseView.extend({ + __bind__: [], + className: 'tab-pane', + tag: 'div', + template: require('kraken/template/dashboard/dashboard-tab'), + constructor: (function(){ + function DashboardTabView(){ + return BaseView.apply(this, arguments); + } + return DashboardTabView; + }()), + initialize: function(){ + return BaseView.prototype.initialize.apply(this, arguments); + }, + getTabId: function(){ + return _.underscored(this.model.get('name')).toLowerCase() + '-graphs-tab'; + }, + toTemplateLocals: function(){ + var json, tab_name; + json = DashboardTabView.__super__.toTemplateLocals.apply(this, arguments); + tab_name = _.underscored(this.model.get('name')).toLowerCase(); + return json.tab_cls = tab_name + "-graphs-pane", json.tab_id = tab_name + "-graphs-tab", json; + } +}); + +}); diff --git a/lib/dashboard/index.js b/lib/dashboard/index.js new file mode 100644 index 0000000..078918a --- /dev/null +++ b/lib/dashboard/index.js @@ -0,0 +1,9 @@ +var models, views; +models = require('kraken/dashboard/dashboard-model'); +views = require('kraken/dashboard/dashboard-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/dashboard/index.mod.js b/lib/dashboard/index.mod.js new file mode 100644 index 0000000..5471d02 --- /dev/null +++ b/lib/dashboard/index.mod.js @@ -0,0 +1,13 @@ +require.define('/node_modules/kraken/dashboard/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var models, views; +models = require('kraken/dashboard/dashboard-model'); +views = require('kraken/dashboard/dashboard-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/data/data-view.js b/lib/data/data-view.js new file mode 100644 index 0000000..08e1688 --- /dev/null +++ b/lib/data/data-view.js @@ -0,0 +1,142 @@ +var Seq, op, BaseView, ViewList, DataSetView, MetricEditView, DataSource, DataView, _ref, _; +Seq = require('seq'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseView = _ref.BaseView, ViewList = _ref.ViewList; +DataSetView = require('kraken/data/dataset-view').DataSetView; +MetricEditView = require('kraken/data/metric-edit-view').MetricEditView; +DataSource = require('kraken/data/datasource-model').DataSource; +/** + * @class DataSet selection and customization UI (root of the `data` tab). + */ +DataView = exports.DataView = BaseView.extend({ + __bind__: ['onMetricsChanged'], + tagName: 'section', + className: 'data-ui', + template: require('kraken/template/data/data'), + datasources: null + /** + * @constructor + */, + constructor: (function(){ + function DataView(){ + return BaseView.apply(this, arguments); + } + return DataView; + }()), + initialize: function(){ + this.graph_id = this.options.graph_id; + BaseView.prototype.initialize.apply(this, arguments); + this.metric_views = new ViewList; + this.datasources = DataSource.getAllSources(); + this.model.metrics.on('add', this.addMetric, this).on('remove', this.removeMetric, this); + return this.model.once('ready', this.onReady, this); + }, + onReady: function(){ + var dataset; + dataset = this.model; + this.model.metrics.each(this.addMetric, this); + this.dataset_view = new DataSetView({ + model: this.model, + graph_id: this.graph_id, + dataset: dataset, + datasources: this.datasources + }); + this.addSubview(this.dataset_view).on('add-metric', this.onMetricsChanged, this).on('remove-metric', this.onMetricsChanged, this).on('select-metric', this.selectMetric, this); + this.render(); + this.triggerReady(); + return this; + } + /** + * Transform the `columns` field to ensure an Array of {label, type} objects. + */, + canonicalizeDataSource: function(ds){ + var cols; + ds.shortName || (ds.shortName = ds.name); + ds.title || (ds.title = ds.name); + ds.subtitle || (ds.subtitle = ''); + cols = ds.columns; + if (_.isArray(cols)) { + ds.metrics = _.map(cols, function(col, idx){ + var label, type; + if (_.isArray(col)) { + label = col[0], type = col[1]; + return { + idx: idx, + label: label, + type: type || 'int' + }; + } else { + return col; + } + }); + } else { + ds.metrics = _.map(cols.labels, function(label, idx){ + return { + idx: idx, + label: label, + type: cols.types[idx] || 'int' + }; + }); + } + return ds; + }, + toTemplateLocals: function(){ + var attrs; + attrs = _.clone(this.model.attributes); + return __import({ + graph_id: this.graph_id, + datasources: this.datasources + }, attrs); + }, + addMetric: function(metric){ + var view; + if (this.metric_views.findByModel(metric)) { + return metric; + } + view = new MetricEditView({ + model: metric, + graph_id: this.graph_id, + dataset: this.model, + datasources: this.datasources + }).on('metric-update', this.onUpdateMetric, this).on('metric-change', this.onUpdateMetric, this); + this.metric_views.push(this.addSubview(view)); + this.renderSubviews(); + return metric; + }, + removeMetric: function(metric){ + var view; + if (!(view = this.metric_views.findByModel(metric))) { + return; + } + this.metric_views.remove(view); + this.removeSubview(view); + return metric; + }, + selectMetric: function(metric){ + var _ref; + this.metric_views.invoke('hide'); + this.metric_edit_view = this.metric_views.findByModel(metric); + if ((_ref = this.metric_edit_view) != null) { + _ref.show(); + } + return _.delay(this.onMetricsChanged, 10); + }, + onMetricsChanged: function(){ + var oldMinHeight, newMinHeight, _ref; + if (!this.dataset_view) { + return; + } + oldMinHeight = parseInt(this.$el.css('min-height')); + newMinHeight = Math.max(this.dataset_view.$el.height(), (_ref = this.metric_edit_view) != null ? _ref.$el.height() : void 8); + return this.$el.css('min-height', newMinHeight); + }, + onUpdateMetric: function(){ + this.trigger('metric-change', this.model, this); + return this.render(); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/data/data-view.mod.js b/lib/data/data-view.mod.js new file mode 100644 index 0000000..73e06f5 --- /dev/null +++ b/lib/data/data-view.mod.js @@ -0,0 +1,146 @@ +require.define('/node_modules/kraken/data/data-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, op, BaseView, ViewList, DataSetView, MetricEditView, DataSource, DataView, _ref, _; +Seq = require('seq'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseView = _ref.BaseView, ViewList = _ref.ViewList; +DataSetView = require('kraken/data/dataset-view').DataSetView; +MetricEditView = require('kraken/data/metric-edit-view').MetricEditView; +DataSource = require('kraken/data/datasource-model').DataSource; +/** + * @class DataSet selection and customization UI (root of the `data` tab). + */ +DataView = exports.DataView = BaseView.extend({ + __bind__: ['onMetricsChanged'], + tagName: 'section', + className: 'data-ui', + template: require('kraken/template/data/data'), + datasources: null + /** + * @constructor + */, + constructor: (function(){ + function DataView(){ + return BaseView.apply(this, arguments); + } + return DataView; + }()), + initialize: function(){ + this.graph_id = this.options.graph_id; + BaseView.prototype.initialize.apply(this, arguments); + this.metric_views = new ViewList; + this.datasources = DataSource.getAllSources(); + this.model.metrics.on('add', this.addMetric, this).on('remove', this.removeMetric, this); + return this.model.once('ready', this.onReady, this); + }, + onReady: function(){ + var dataset; + dataset = this.model; + this.model.metrics.each(this.addMetric, this); + this.dataset_view = new DataSetView({ + model: this.model, + graph_id: this.graph_id, + dataset: dataset, + datasources: this.datasources + }); + this.addSubview(this.dataset_view).on('add-metric', this.onMetricsChanged, this).on('remove-metric', this.onMetricsChanged, this).on('select-metric', this.selectMetric, this); + this.render(); + this.triggerReady(); + return this; + } + /** + * Transform the `columns` field to ensure an Array of {label, type} objects. + */, + canonicalizeDataSource: function(ds){ + var cols; + ds.shortName || (ds.shortName = ds.name); + ds.title || (ds.title = ds.name); + ds.subtitle || (ds.subtitle = ''); + cols = ds.columns; + if (_.isArray(cols)) { + ds.metrics = _.map(cols, function(col, idx){ + var label, type; + if (_.isArray(col)) { + label = col[0], type = col[1]; + return { + idx: idx, + label: label, + type: type || 'int' + }; + } else { + return col; + } + }); + } else { + ds.metrics = _.map(cols.labels, function(label, idx){ + return { + idx: idx, + label: label, + type: cols.types[idx] || 'int' + }; + }); + } + return ds; + }, + toTemplateLocals: function(){ + var attrs; + attrs = _.clone(this.model.attributes); + return __import({ + graph_id: this.graph_id, + datasources: this.datasources + }, attrs); + }, + addMetric: function(metric){ + var view; + if (this.metric_views.findByModel(metric)) { + return metric; + } + view = new MetricEditView({ + model: metric, + graph_id: this.graph_id, + dataset: this.model, + datasources: this.datasources + }).on('metric-update', this.onUpdateMetric, this).on('metric-change', this.onUpdateMetric, this); + this.metric_views.push(this.addSubview(view)); + this.renderSubviews(); + return metric; + }, + removeMetric: function(metric){ + var view; + if (!(view = this.metric_views.findByModel(metric))) { + return; + } + this.metric_views.remove(view); + this.removeSubview(view); + return metric; + }, + selectMetric: function(metric){ + var _ref; + this.metric_views.invoke('hide'); + this.metric_edit_view = this.metric_views.findByModel(metric); + if ((_ref = this.metric_edit_view) != null) { + _ref.show(); + } + return _.delay(this.onMetricsChanged, 10); + }, + onMetricsChanged: function(){ + var oldMinHeight, newMinHeight, _ref; + if (!this.dataset_view) { + return; + } + oldMinHeight = parseInt(this.$el.css('min-height')); + newMinHeight = Math.max(this.dataset_view.$el.height(), (_ref = this.metric_edit_view) != null ? _ref.$el.height() : void 8); + return this.$el.css('min-height', newMinHeight); + }, + onUpdateMetric: function(){ + this.trigger('metric-change', this.model, this); + return this.render(); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/data/dataset-model.js b/lib/data/dataset-model.js new file mode 100644 index 0000000..2f75baa --- /dev/null +++ b/lib/data/dataset-model.js @@ -0,0 +1,184 @@ +var Seq, ColorBrewer, op, BaseModel, BaseList, Metric, MetricList, DataSource, DataSourceList, DataSet, _ref, _; +Seq = require('seq'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +_ref = require('kraken/data/metric-model'), Metric = _ref.Metric, MetricList = _ref.MetricList; +_ref = require('kraken/data/datasource-model'), DataSource = _ref.DataSource, DataSourceList = _ref.DataSourceList; +/** + * @class + */ +DataSet = exports.DataSet = BaseModel.extend({ + urlRoot: '/datasets' + /** + * @type DataSourceList + */, + sources: null + /** + * @type MetricList + */, + metrics: null, + defaults: function(){ + return { + palette: null, + lines: [], + metrics: [] + }; + }, + constructor: (function(){ + function DataSet(attributes, opts){ + attributes == null && (attributes = {}); + this.metrics = new MetricList(attributes.metrics); + return BaseModel.call(this, attributes, opts); + } + return DataSet; + }()), + initialize: function(){ + BaseModel.prototype.initialize.apply(this, arguments); + this.set('metrics', this.metrics, { + silent: true + }); + return this.on('change:metrics', this.onMetricChange, this); + }, + load: function(opts){ + var _this = this; + opts == null && (opts = {}); + if (opts.force) { + this.resetReady(); + } + if (this.loading || this.ready) { + return this; + } + if (!this.metrics.length) { + return this.triggerReady(); + } + this.wait(); + this.loading = true; + this.trigger('load', this); + Seq(this.metrics.models).parEach_(function(next, metric){ + return metric.once('ready', next.ok).load(); + }).seq(function(){ + _this.loading = false; + _this.unwait(); + return _this.triggerReady(); + }); + return 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: function(key, value, opts){ + var values, _ref; + if (_.isObject(key) && key != null) { + _ref = [key, value], values = _ref[0], opts = _ref[1]; + } else { + values = (_ref = {}, _ref[key + ""] = value, _ref); + } + opts || (opts = {}); + for (key in values) { + value = values[key]; + if (!(key === 'metrics' && _.isArray(value))) { + continue; + } + this.metrics.reset(value); + delete values[key]; + if (!opts.silent) { + DataSet.__super__.set.call(this, 'metrics', value, { + silent: true + }); + DataSet.__super__.set.call(this, 'metrics', this.metrics, opts); + } + } + return DataSet.__super__.set.call(this, values, opts); + }, + toJSON: function(){ + var json; + json = DataSet.__super__.toJSON.apply(this, arguments); + delete json.id; + return json; + } + /* * * * TimeSeriesData interface * * * {{{ */ + /** + * @returns {Array} The reified dataset, materialized to a list of rows including timestamps. + */, + getData: function(){ + var columns; + if (!this.ready) { + return []; + } + columns = this.getColumns(); + if (columns != null && columns.length) { + return _.zip.apply(_, columns); + } else { + return []; + } + } + /** + * @returns {Array} List of all columns (including date column). + */, + getColumns: function(){ + if (!this.ready) { + return []; + } + return _.compact([this.getDateColumn()].concat(this.getDataColumns())); + } + /** + * @returns {Array} The date column. + */, + getDateColumn: function(){ + var dates, maxLen; + if (!this.ready) { + return []; + } + dates = this.metrics.onlyOk().invoke('getDateColumn'); + maxLen = _.max(_.pluck(dates, 'length')); + return _.find(dates, function(it){ + return it.length === maxLen; + }); + } + /** + * @returns {Array} List of all columns except the date column. + */, + getDataColumns: function(){ + if (!this.ready) { + return []; + } + return this.metrics.onlyOk().invoke('getData'); + } + /** + * @returns {Array} List of column labels. + */, + getLabels: function(){ + if (!this.ready) { + return []; + } + return ['Date'].concat(this.metrics.onlyOk().invoke('getLabel')); + }, + getColors: function(){ + if (!this.ready) { + return []; + } + return this.metrics.onlyOk().invoke('getColor'); + }, + newMetric: function(){ + var index, m, _this = this; + index = this.metrics.length; + this.metrics.add(m = new Metric({ + index: index, + color: ColorBrewer.Spectral[11][index] + })); + m.on('ready', function(){ + return _this.trigger('metric-data-loaded', _this, m); + }); + return m; + }, + onMetricChange: function(){ + this.resetReady(); + return this.load(); + } +}); \ No newline at end of file diff --git a/lib/data/dataset-model.mod.js b/lib/data/dataset-model.mod.js new file mode 100644 index 0000000..f3c47b1 --- /dev/null +++ b/lib/data/dataset-model.mod.js @@ -0,0 +1,188 @@ +require.define('/node_modules/kraken/data/dataset-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, ColorBrewer, op, BaseModel, BaseList, Metric, MetricList, DataSource, DataSourceList, DataSet, _ref, _; +Seq = require('seq'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +_ref = require('kraken/data/metric-model'), Metric = _ref.Metric, MetricList = _ref.MetricList; +_ref = require('kraken/data/datasource-model'), DataSource = _ref.DataSource, DataSourceList = _ref.DataSourceList; +/** + * @class + */ +DataSet = exports.DataSet = BaseModel.extend({ + urlRoot: '/datasets' + /** + * @type DataSourceList + */, + sources: null + /** + * @type MetricList + */, + metrics: null, + defaults: function(){ + return { + palette: null, + lines: [], + metrics: [] + }; + }, + constructor: (function(){ + function DataSet(attributes, opts){ + attributes == null && (attributes = {}); + this.metrics = new MetricList(attributes.metrics); + return BaseModel.call(this, attributes, opts); + } + return DataSet; + }()), + initialize: function(){ + BaseModel.prototype.initialize.apply(this, arguments); + this.set('metrics', this.metrics, { + silent: true + }); + return this.on('change:metrics', this.onMetricChange, this); + }, + load: function(opts){ + var _this = this; + opts == null && (opts = {}); + if (opts.force) { + this.resetReady(); + } + if (this.loading || this.ready) { + return this; + } + if (!this.metrics.length) { + return this.triggerReady(); + } + this.wait(); + this.loading = true; + this.trigger('load', this); + Seq(this.metrics.models).parEach_(function(next, metric){ + return metric.once('ready', next.ok).load(); + }).seq(function(){ + _this.loading = false; + _this.unwait(); + return _this.triggerReady(); + }); + return 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: function(key, value, opts){ + var values, _ref; + if (_.isObject(key) && key != null) { + _ref = [key, value], values = _ref[0], opts = _ref[1]; + } else { + values = (_ref = {}, _ref[key + ""] = value, _ref); + } + opts || (opts = {}); + for (key in values) { + value = values[key]; + if (!(key === 'metrics' && _.isArray(value))) { + continue; + } + this.metrics.reset(value); + delete values[key]; + if (!opts.silent) { + DataSet.__super__.set.call(this, 'metrics', value, { + silent: true + }); + DataSet.__super__.set.call(this, 'metrics', this.metrics, opts); + } + } + return DataSet.__super__.set.call(this, values, opts); + }, + toJSON: function(){ + var json; + json = DataSet.__super__.toJSON.apply(this, arguments); + delete json.id; + return json; + } + /* * * * TimeSeriesData interface * * * {{{ */ + /** + * @returns {Array} The reified dataset, materialized to a list of rows including timestamps. + */, + getData: function(){ + var columns; + if (!this.ready) { + return []; + } + columns = this.getColumns(); + if (columns != null && columns.length) { + return _.zip.apply(_, columns); + } else { + return []; + } + } + /** + * @returns {Array} List of all columns (including date column). + */, + getColumns: function(){ + if (!this.ready) { + return []; + } + return _.compact([this.getDateColumn()].concat(this.getDataColumns())); + } + /** + * @returns {Array} The date column. + */, + getDateColumn: function(){ + var dates, maxLen; + if (!this.ready) { + return []; + } + dates = this.metrics.onlyOk().invoke('getDateColumn'); + maxLen = _.max(_.pluck(dates, 'length')); + return _.find(dates, function(it){ + return it.length === maxLen; + }); + } + /** + * @returns {Array} List of all columns except the date column. + */, + getDataColumns: function(){ + if (!this.ready) { + return []; + } + return this.metrics.onlyOk().invoke('getData'); + } + /** + * @returns {Array} List of column labels. + */, + getLabels: function(){ + if (!this.ready) { + return []; + } + return ['Date'].concat(this.metrics.onlyOk().invoke('getLabel')); + }, + getColors: function(){ + if (!this.ready) { + return []; + } + return this.metrics.onlyOk().invoke('getColor'); + }, + newMetric: function(){ + var index, m, _this = this; + index = this.metrics.length; + this.metrics.add(m = new Metric({ + index: index, + color: ColorBrewer.Spectral[11][index] + })); + m.on('ready', function(){ + return _this.trigger('metric-data-loaded', _this, m); + }); + return m; + }, + onMetricChange: function(){ + this.resetReady(); + return this.load(); + } +}); + +}); diff --git a/lib/data/dataset-view.js b/lib/data/dataset-view.js new file mode 100644 index 0000000..d1296fd --- /dev/null +++ b/lib/data/dataset-view.js @@ -0,0 +1,131 @@ +var op, BaseView, DataSetView, DataSetMetricView, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseView = require('kraken/base').BaseView; +/** + * @class + */ +DataSetView = exports.DataSetView = BaseView.extend({ + 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(){ + function DataSetView(){ + return BaseView.apply(this, arguments); + } + return DataSetView; + }()), + initialize: function(){ + var _ref; + _ref = this.options, this.graph_id = _ref.graph_id, this.datasources = _ref.datasources, this.dataset = _ref.dataset; + BaseView.prototype.initialize.apply(this, arguments); + this.views_by_cid = {}; + this.model.on('ready', this.addAllMetrics, this); + return this.model.metrics.on('add', this.addMetric, this).on('remove', this.removeMetric, this).on('change', this.onMetricChange, this).on('reset', this.addAllMetrics, this); + }, + addMetric: function(metric){ + var that, view; + if (that = this.views_by_cid[metric.cid]) { + this.removeSubview(that); + delete this.views_by_cid[metric.cid]; + } + view = this.addSubview(new DataSetMetricView({ + model: metric, + graph_id: this.graph_id + })); + this.views_by_cid[metric.cid] = view; + this.trigger('add-metric', metric, view, this); + this.render(); + return view; + }, + removeMetric: function(metric){ + var view; + if (metric instanceof jQuery.Event || metric instanceof Event) { + metric = this.getMetricForElement(metric.target); + } + if (!metric) { + return; + } + if (view = this.views_by_cid[metric.cid]) { + this.removeSubview(view); + delete this.views_by_cid[metric.cid]; + this.trigger('remove-metric', metric, view, this); + } + return view; + }, + addAllMetrics: function(){ + this.removeAllSubviews(); + this.model.metrics.each(this.addMetric, this); + return this; + }, + selectMetric: function(metric){ + var view; + if (metric instanceof jQuery.Event || metric instanceof Event) { + metric = this.getMetricForElement(metric.target); + } + if (!metric) { + return; + } + view = this.active_view = this.views_by_cid[metric.cid]; + this.$('.metrics .dataset-metric').removeClass('metric-active'); + view.$el.addClass('metric-active'); + view.$('.activity-arrow').css('font-size', 2 + view.$el.height()); + this.trigger('select-metric', metric, view, this); + return this; + }, + onMetricChange: function(metric){ + var view; + if (!(view = this.views_by_cid[metric != null ? metric.cid : void 8])) { + return; + } + return view.$('.activity-arrow:visible').css('font-size', 2 + view.$el.height()); + }, + onNewMetric: function(){ + this.model.newMetric(); + return false; + }, + onDeleteMetric: function(evt){ + var metric; + metric = this.getMetricForElement(evt.target); + this.model.metrics.remove(metric); + return false; + }, + getMetricForElement: function(el){ + return $(el).parents('.dataset-metric').eq(0).data('model'); + } +}); +/** + * @class + */ +DataSetMetricView = exports.DataSetMetricView = BaseView.extend({ + tagName: 'tr', + className: 'dataset-metric metric', + template: require('kraken/template/data/dataset-metric'), + constructor: (function(){ + function DataSetMetricView(){ + return BaseView.apply(this, arguments); + } + return DataSetMetricView; + }()), + initialize: function(){ + this.graph_id = this.options.graph_id; + BaseView.prototype.initialize.apply(this, arguments); + return this.on('update', this.onUpdate, this); + }, + toTemplateLocals: function(){ + var m, ts; + m = DataSetMetricView.__super__.toTemplateLocals.apply(this, arguments); + return m.graph_id = this.graph_id, m.label = this.model.getLabel(), m.viewClasses = _.compact([this.model.isOk() ? 'valid' : 'invalid', m.visible ? 'visible' : 'hidden', m.disabled ? 'disabled' : void 8]).map(function(it){ + ret