Adds compiled JS source files to be published with the npm distribution.
authorDavid Schoonover <dsc@wikimedia.org>
Tue, 10 Jul 2012 13:19:39 +0000 (06:19 -0700)
committerDavid Schoonover <dsc@wikimedia.org>
Tue, 10 Jul 2012 13:19:39 +0000 (06:19 -0700)
175 files changed:
lib/app.js [new file with mode: 0644]
lib/app.mod.js [new file with mode: 0644]
lib/base/base-mixin.js [new file with mode: 0644]
lib/base/base-mixin.mod.js [new file with mode: 0644]
lib/base/base-model.js [new file with mode: 0644]
lib/base/base-model.mod.js [new file with mode: 0644]
lib/base/base-view.js [new file with mode: 0644]
lib/base/base-view.mod.js [new file with mode: 0644]
lib/base/base.js [new file with mode: 0644]
lib/base/base.mod.js [new file with mode: 0644]
lib/base/cascading-model.js [new file with mode: 0644]
lib/base/cascading-model.mod.js [new file with mode: 0644]
lib/base/data-binding.js [new file with mode: 0644]
lib/base/data-binding.mod.js [new file with mode: 0644]
lib/base/index.js [new file with mode: 0644]
lib/base/index.mod.js [new file with mode: 0644]
lib/base/model-cache.js [new file with mode: 0644]
lib/base/model-cache.mod.js [new file with mode: 0644]
lib/base/scaffold/index.js [new file with mode: 0644]
lib/base/scaffold/index.mod.js [new file with mode: 0644]
lib/base/scaffold/scaffold-model.js [new file with mode: 0644]
lib/base/scaffold/scaffold-model.mod.js [new file with mode: 0644]
lib/base/scaffold/scaffold-view.js [new file with mode: 0644]
lib/base/scaffold/scaffold-view.mod.js [new file with mode: 0644]
lib/chart/chart-type.js [new file with mode: 0644]
lib/chart/chart-type.mod.js [new file with mode: 0644]
lib/chart/index.js [new file with mode: 0644]
lib/chart/index.mod.js [new file with mode: 0644]
lib/chart/option/chart-option-model.js [new file with mode: 0644]
lib/chart/option/chart-option-model.mod.js [new file with mode: 0644]
lib/chart/option/chart-option-view.js [new file with mode: 0644]
lib/chart/option/chart-option-view.mod.js [new file with mode: 0644]
lib/chart/option/index.js [new file with mode: 0644]
lib/chart/option/index.mod.js [new file with mode: 0644]
lib/chart/type/d3-chart.js [new file with mode: 0644]
lib/chart/type/d3-chart.mod.js [new file with mode: 0644]
lib/chart/type/d3/d3-bar-element.js [new file with mode: 0644]
lib/chart/type/d3/d3-bar-element.mod.js [new file with mode: 0644]
lib/chart/type/d3/d3-chart-element.js [new file with mode: 0644]
lib/chart/type/d3/d3-chart-element.mod.js [new file with mode: 0644]
lib/chart/type/d3/d3-line-element.js [new file with mode: 0644]
lib/chart/type/d3/d3-line-element.mod.js [new file with mode: 0644]
lib/chart/type/d3/index.js [new file with mode: 0644]
lib/chart/type/d3/index.mod.js [new file with mode: 0644]
lib/chart/type/dygraphs.js [new file with mode: 0644]
lib/chart/type/dygraphs.mod.js [new file with mode: 0644]
lib/chart/type/index.js [new file with mode: 0644]
lib/chart/type/index.mod.js [new file with mode: 0644]
lib/dashboard/dashboard-model.js [new file with mode: 0644]
lib/dashboard/dashboard-model.mod.js [new file with mode: 0644]
lib/dashboard/dashboard-view.js [new file with mode: 0644]
lib/dashboard/dashboard-view.mod.js [new file with mode: 0644]
lib/dashboard/index.js [new file with mode: 0644]
lib/dashboard/index.mod.js [new file with mode: 0644]
lib/data/data-view.js [new file with mode: 0644]
lib/data/data-view.mod.js [new file with mode: 0644]
lib/data/dataset-model.js [new file with mode: 0644]
lib/data/dataset-model.mod.js [new file with mode: 0644]
lib/data/dataset-view.js [new file with mode: 0644]
lib/data/dataset-view.mod.js [new file with mode: 0644]
lib/data/datasource-model.js [new file with mode: 0644]
lib/data/datasource-model.mod.js [new file with mode: 0644]
lib/data/datasource-ui-view.js [new file with mode: 0644]
lib/data/datasource-ui-view.mod.js [new file with mode: 0644]
lib/data/datasource-view.js [new file with mode: 0644]
lib/data/datasource-view.mod.js [new file with mode: 0644]
lib/data/index.js [new file with mode: 0644]
lib/data/index.mod.js [new file with mode: 0644]
lib/data/metric-edit-view.js [new file with mode: 0644]
lib/data/metric-edit-view.mod.js [new file with mode: 0644]
lib/data/metric-model.js [new file with mode: 0644]
lib/data/metric-model.mod.js [new file with mode: 0644]
lib/data/project-colors.js [new file with mode: 0644]
lib/data/project-colors.mod.js [new file with mode: 0644]
lib/graph/graph-display-view.js [new file with mode: 0644]
lib/graph/graph-display-view.mod.js [new file with mode: 0644]
lib/graph/graph-edit-view.js [new file with mode: 0644]
lib/graph/graph-edit-view.mod.js [new file with mode: 0644]
lib/graph/graph-list-view.js [new file with mode: 0644]
lib/graph/graph-list-view.mod.js [new file with mode: 0644]
lib/graph/graph-model.js [new file with mode: 0644]
lib/graph/graph-model.mod.js [new file with mode: 0644]
lib/graph/graph-view.js [new file with mode: 0644]
lib/graph/graph-view.mod.js [new file with mode: 0644]
lib/graph/index.js [new file with mode: 0644]
lib/graph/index.mod.js [new file with mode: 0644]
lib/main-dashboard.js [new file with mode: 0644]
lib/main-display.js [new file with mode: 0644]
lib/main-edit.js [new file with mode: 0644]
lib/main-geo.js [new file with mode: 0644]
lib/main-graph-list.js [new file with mode: 0644]
lib/template/browser-helpers.jade.js [new file with mode: 0644]
lib/template/chart/chart-option.jade.js [new file with mode: 0644]
lib/template/chart/chart-option.jade.mod.js [new file with mode: 0644]
lib/template/chart/chart-scaffold.jade.js [new file with mode: 0644]
lib/template/chart/chart-scaffold.jade.mod.js [new file with mode: 0644]
lib/template/dashboard/dashboard-tab.jade.js [new file with mode: 0644]
lib/template/dashboard/dashboard-tab.jade.mod.js [new file with mode: 0644]
lib/template/dashboard/dashboard.jade.js [new file with mode: 0644]
lib/template/dashboard/dashboard.jade.mod.js [new file with mode: 0644]
lib/template/data/data.jade.js [new file with mode: 0644]
lib/template/data/data.jade.mod.js [new file with mode: 0644]
lib/template/data/dataset-metric.jade.js [new file with mode: 0644]
lib/template/data/dataset-metric.jade.mod.js [new file with mode: 0644]
lib/template/data/dataset.jade.js [new file with mode: 0644]
lib/template/data/dataset.jade.mod.js [new file with mode: 0644]
lib/template/data/datasource-ui.jade.js [new file with mode: 0644]
lib/template/data/datasource-ui.jade.mod.js [new file with mode: 0644]
lib/template/data/datasource.jade.js [new file with mode: 0644]
lib/template/data/datasource.jade.mod.js [new file with mode: 0644]
lib/template/data/metric-edit.jade.js [new file with mode: 0644]
lib/template/data/metric-edit.jade.mod.js [new file with mode: 0644]
lib/template/graph/graph-display.jade.js [new file with mode: 0644]
lib/template/graph/graph-display.jade.mod.js [new file with mode: 0644]
lib/template/graph/graph-edit.jade.js [new file with mode: 0644]
lib/template/graph/graph-edit.jade.mod.js [new file with mode: 0644]
lib/template/graph/graph-list.jade.js [new file with mode: 0644]
lib/template/graph/graph-list.jade.mod.js [new file with mode: 0644]
lib/util/backbone.js [new file with mode: 0644]
lib/util/backbone.mod.js [new file with mode: 0644]
lib/util/cascade.js [new file with mode: 0644]
lib/util/cascade.mod.js [new file with mode: 0644]
lib/util/event/index.js [new file with mode: 0644]
lib/util/event/index.mod.js [new file with mode: 0644]
lib/util/event/ready-emitter.js [new file with mode: 0644]
lib/util/event/ready-emitter.mod.js [new file with mode: 0644]
lib/util/event/waiting-emitter.js [new file with mode: 0644]
lib/util/event/waiting-emitter.mod.js [new file with mode: 0644]
lib/util/formatters.js [new file with mode: 0644]
lib/util/formatters.mod.js [new file with mode: 0644]
lib/util/index.js [new file with mode: 0644]
lib/util/index.mod.js [new file with mode: 0644]
lib/util/op.js [new file with mode: 0644]
lib/util/op.mod.js [new file with mode: 0644]
lib/util/parser.js [new file with mode: 0644]
lib/util/parser.mod.js [new file with mode: 0644]
lib/util/timeseries/csv.js [new file with mode: 0644]
lib/util/timeseries/csv.mod.js [new file with mode: 0644]
lib/util/timeseries/index.js [new file with mode: 0644]
lib/util/timeseries/index.mod.js [new file with mode: 0644]
lib/util/timeseries/timeseries.js [new file with mode: 0644]
lib/util/timeseries/timeseries.mod.js [new file with mode: 0644]
lib/util/underscore/array.js [new file with mode: 0644]
lib/util/underscore/array.mod.js [new file with mode: 0644]
lib/util/underscore/class.js [new file with mode: 0644]
lib/util/underscore/class.mod.js [new file with mode: 0644]
lib/util/underscore/function.js [new file with mode: 0644]
lib/util/underscore/function.mod.js [new file with mode: 0644]
lib/util/underscore/index.js [new file with mode: 0644]
lib/util/underscore/index.mod.js [new file with mode: 0644]
lib/util/underscore/kv.js [new file with mode: 0644]
lib/util/underscore/kv.mod.js [new file with mode: 0644]
lib/util/underscore/object.js [new file with mode: 0644]
lib/util/underscore/object.mod.js [new file with mode: 0644]
lib/util/underscore/string.js [new file with mode: 0644]
lib/util/underscore/string.mod.js [new file with mode: 0644]
www/css/bootstrap-variables.css [new file with mode: 0644]
www/css/chart.css [new file with mode: 0644]
www/css/colors.css [new file with mode: 0644]
www/css/dashboard.css [new file with mode: 0644]
www/css/data.css [new file with mode: 0644]
www/css/docs.css [new file with mode: 0644]
www/css/geo-display.css [new file with mode: 0644]
www/css/graph-display-print.css [new file with mode: 0644]
www/css/graph-display.css [new file with mode: 0644]
www/css/graph.css [new file with mode: 0644]
www/css/hicons.css [new file with mode: 0644]
www/css/layout.css [new file with mode: 0644]
www/css/mixins.css [new file with mode: 0644]
www/css/text.css [new file with mode: 0644]
www/schema/d3/d3-bar.json [new file with mode: 0644]
www/schema/d3/d3-chart.json [new file with mode: 0644]
www/schema/d3/d3-geo-world.json [new file with mode: 0644]
www/schema/d3/d3-line.json [new file with mode: 0644]
www/schema/dygraph.json [new file with mode: 0644]

diff --git a/lib/app.js b/lib/app.js
new file mode 100644 (file)
index 0000000..b4937f3
--- /dev/null
@@ -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 (file)
index 0000000..e816fc5
--- /dev/null
@@ -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 (file)
index 0000000..eba7b88
--- /dev/null
@@ -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<String>
+   */,
+  __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 (file)
index 0000000..e2c68e2
--- /dev/null
@@ -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<String>
+   */,
+  __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 (file)
index 0000000..7a4a6d6
--- /dev/null
@@ -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 (file)
index 0000000..1fdd53b
--- /dev/null
@@ -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 (file)
index 0000000..9dec625
--- /dev/null
@@ -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 (file)
index 0000000..2105eea
--- /dev/null
@@ -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 (file)
index 0000000..e3f4bce
--- /dev/null
@@ -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<String>
+   */
+  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 (file)
index 0000000..68ac0c5
--- /dev/null
@@ -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<String>
+   */
+  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 (file)
index 0000000..8a1f89c
--- /dev/null
@@ -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 (file)
index 0000000..5fc62d5
--- /dev/null
@@ -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 (file)
index 0000000..9f1835c
--- /dev/null
@@ -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 (file)
index 0000000..957bc2c
--- /dev/null
@@ -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 (file)
index 0000000..1c9458c
--- /dev/null
@@ -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 (file)
index 0000000..a599753
--- /dev/null
@@ -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 (file)
index 0000000..f07c776
--- /dev/null
@@ -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<Backbone.Model>
+   */
+  prototype.ModelType = null;
+  /**
+   * Collection holding the cached Models.
+   * @private
+   * @type Backbone.Collection
+   */
+  prototype.cache = null;
+  /**
+   * @constructor
+   * @param {Class<Backbone.Model>} [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<Backbone.Collection>} [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<Backbone.Model>} [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<String>} 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<String>} 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 (file)
index 0000000..4e529f9
--- /dev/null
@@ -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<Backbone.Model>
+   */
+  prototype.ModelType = null;
+  /**
+   * Collection holding the cached Models.
+   * @private
+   * @type Backbone.Collection
+   */
+  prototype.cache = null;
+  /**
+   * @constructor
+   * @param {Class<Backbone.Model>} [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<Backbone.Collection>} [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<Backbone.Model>} [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<String>} 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<String>} 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 (file)
index 0000000..c0ee89d
--- /dev/null
@@ -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 (file)
index 0000000..2278e76
--- /dev/null
@@ -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 (file)
index 0000000..948a703
--- /dev/null
@@ -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 (file)
index 0000000..ee06f50
--- /dev/null
@@ -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 (file)
index 0000000..2bc3791
--- /dev/null
@@ -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 $("<label class=\"name\" for=\"" + locals.id + "\">" + locals.name + "</label>\n<input class=\"value\" type=\"text\" id=\"" + locals.id + "\" name=\"" + locals.id + "\" value=\"" + locals.value + "\">");
+  },
+  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 (file)
index 0000000..f71a8da
--- /dev/null
@@ -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 $("<label class=\"name\" for=\"" + locals.id + "\">" + locals.name + "</label>\n<input class=\"value\" type=\"text\" id=\"" + locals.id + "\" name=\"" + locals.id + "\" value=\"" + locals.value + "\">");
+  },
+  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 (file)
index 0000000..037682a
--- /dev/null
@@ -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 (file)
index 0000000..40aaea3
--- /dev/null
@@ -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 (file)
index 0000000..21c43f1
--- /dev/null
@@ -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 (file)
index 0000000..7734fba
--- /dev/null
@@ -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 (file)
index 0000000..4b7382f
--- /dev/null
@@ -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 (file)
index 0000000..50446ba
--- /dev/null
@@ -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 (file)
index 0000000..99a9a41
--- /dev/null
@@ -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 (file)
index 0000000..8bf441a
--- /dev/null
@@ -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 (file)
index 0000000..09d95b7
--- /dev/null
@@ -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 (file)
index 0000000..15c866d
--- /dev/null
@@ -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 (file)
index 0000000..adce99c
--- /dev/null
@@ -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 (file)
index 0000000..f6817e0
--- /dev/null
@@ -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 (file)
index 0000000..b1248b2
--- /dev/null
@@ -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 (file)
index 0000000..cd8d26a
--- /dev/null
@@ -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 (file)
index 0000000..d61b091
--- /dev/null
@@ -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 (file)
index 0000000..4a9e29e
--- /dev/null
@@ -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 (file)
index 0000000..c6c5867
--- /dev/null
@@ -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 (file)
index 0000000..c0458f7
--- /dev/null
@@ -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 (file)
index 0000000..c1193b7
--- /dev/null
@@ -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 (file)
index 0000000..9c2a284
--- /dev/null
@@ -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 (file)
index 0000000..59865da
--- /dev/null
@@ -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 "<span class='value'><span class='whole'>" + whole + "</span><span class='fraction'>" + fraction + "</span><span class='suffix'>" + suffix + "</span></span>";
+  };
+  /**
+   * 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 (file)
index 0000000..68e7d1f
--- /dev/null
@@ -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 "<span class='value'><span class='whole'>" + whole + "</span><span class='fraction'>" + fraction + "</span><span class='suffix'>" + suffix + "</span></span>";
+  };
+  /**
+   * 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 (file)
index 0000000..e69de29
diff --git a/lib/chart/type/index.mod.js b/lib/chart/type/index.mod.js
new file mode 100644 (file)
index 0000000..c1940bf
--- /dev/null
@@ -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 (file)
index 0000000..8e26e25
--- /dev/null
@@ -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 (file)
index 0000000..7e61eb7
--- /dev/null
@@ -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 (file)
index 0000000..37b4d79
--- /dev/null
@@ -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("<li class='" + tabId + "-button'><a href='#" + tabId + "' data-toggle='tab'>" + tab.name + "</a></li>");
+    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 (file)
index 0000000..cc0228a
--- /dev/null
@@ -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("<li class='" + tabId + "-button'><a href='#" + tabId + "' data-toggle='tab'>" + tab.name + "</a></li>");
+    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 (file)
index 0000000..078918a
--- /dev/null
@@ -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 (file)
index 0000000..5471d02
--- /dev/null
@@ -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 (file)
index 0000000..08e1688
--- /dev/null
@@ -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 (file)
index 0000000..73e06f5
--- /dev/null
@@ -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 (file)
index 0000000..2f75baa
--- /dev/null
@@ -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<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<Array>} List of all columns (including date column).
+   */,
+  getColumns: function(){
+    if (!this.ready) {
+      return [];
+    }
+    return _.compact([this.getDateColumn()].concat(this.getDataColumns()));
+  }
+  /**
+   * @returns {Array<Date>} 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<Array>} List of all columns except the date column.
+   */,
+  getDataColumns: function(){
+    if (!this.ready) {
+      return [];
+    }
+    return this.metrics.onlyOk().invoke('getData');
+  }
+  /**
+   * @returns {Array<String>} 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 (file)
index 0000000..f3c47b1
--- /dev/null
@@ -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<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<Array>} List of all columns (including date column).
+   */,
+  getColumns: function(){
+    if (!this.ready) {
+      return [];
+    }
+    return _.compact([this.getDateColumn()].concat(this.getDataColumns()));
+  }
+  /**
+   * @returns {Array<Date>} 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<Array>} List of all columns except the date column.
+   */,
+  getDataColumns: function(){
+    if (!this.ready) {
+      return [];
+    }
+    return this.metrics.onlyOk().invoke('getData');
+  }
+  /**
+   * @returns {Array<String>} 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 (file)
index 0000000..d1296fd
--- /dev/null
@@ -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){
+      return "metric-" + it;
+    }).join(' '), m.source = m.source_id && m.source_col ? m.source_id + "[" + m.source_col + "]" : 'No source', m.timespan = _.every(ts = m.timespan, op.ok) ? ts.start + " to " + ts.end + " by " + ts.step : '&mdash;', m;
+  },
+  onUpdate: function(){
+    return this.$('.col-color').css('color', this.model.get('color'));
+  }
+});
\ No newline at end of file
diff --git a/lib/data/dataset-view.mod.js b/lib/data/dataset-view.mod.js
new file mode 100644 (file)
index 0000000..b5aec94
--- /dev/null
@@ -0,0 +1,135 @@
+require.define('/node_modules/kraken/data/dataset-view.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+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){
+      return "metric-" + it;
+    }).join(' '), m.source = m.source_id && m.source_col ? m.source_id + "[" + m.source_col + "]" : 'No source', m.timespan = _.every(ts = m.timespan, op.ok) ? ts.start + " to " + ts.end + " by " + ts.step : '&mdash;', m;
+  },
+  onUpdate: function(){
+    return this.$('.col-color').css('color', this.model.get('color'));
+  }
+});
+
+});
diff --git a/lib/data/datasource-model.js b/lib/data/datasource-model.js
new file mode 100644 (file)
index 0000000..8b72068
--- /dev/null
@@ -0,0 +1,217 @@
+var op, TimeSeriesData, CSVData, BaseModel, BaseList, ModelCache, Metric, MetricList, DataSource, DataSourceList, ALL_SOURCES, sourceCache, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/util/timeseries'), TimeSeriesData = _ref.TimeSeriesData, CSVData = _ref.CSVData;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, ModelCache = _ref.ModelCache;
+_ref = require('kraken/data/metric-model'), Metric = _ref.Metric, MetricList = _ref.MetricList;
+/**
+ * @class
+ */
+DataSource = exports.DataSource = BaseModel.extend({
+  __bind__: ['onLoadDataSuccess', 'onLoadDataError'],
+  urlRoot: '/datasources',
+  ready: false
+  /**
+   * Parsed data for this datasource.
+   * @type Array
+   */,
+  data: null,
+  defaults: function(){
+    return {
+      id: '',
+      url: '',
+      format: 'json',
+      name: '',
+      shortName: '',
+      title: '',
+      subtitle: '',
+      desc: '',
+      notes: '',
+      timespan: {
+        start: null,
+        end: null,
+        step: '1mo'
+      },
+      columns: [],
+      chart: {
+        chartType: 'dygraphs',
+        options: {}
+      }
+    };
+  },
+  url: function(){
+    return "/datasources/" + this.id + ".json";
+  },
+  constructor: (function(){
+    function DataSource(){
+      return BaseModel.apply(this, arguments);
+    }
+    return DataSource;
+  }()),
+  initialize: function(){
+    this.attributes = this.canonicalize(this.attributes);
+    BaseModel.prototype.initialize.apply(this, arguments);
+    this.constructor.register(this);
+    this.metrics = new MetricList(this.attributes.metrics);
+    return this.on('change:metrics', this.onMetricChange, this);
+  },
+  canonicalize: 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 {
+          col.type || (col.type = 'int');
+          return col;
+        }
+      });
+    } else {
+      ds.metrics = _.map(cols.labels, function(label, idx){
+        return {
+          idx: idx,
+          label: label,
+          type: cols.types[idx] || 'int'
+        };
+      });
+    }
+    return ds;
+  },
+  loadAll: function(){
+    this.loader({
+      start: function(){
+        var _this = this;
+        return Seq().seq_(function(next){
+          _this.once('fetch-success', next.ok);
+          return _this.loadModel();
+        }).seq_(function(next){
+          _this.once('load-data-success', next.ok);
+          return _this.loadData();
+        }).seq(function(){
+          return _this.trigger('load-success', _this);
+        });
+      }
+    });
+    return this;
+  },
+  loadData: function(){
+    this.wait();
+    this.trigger('load-data', this);
+    if (this.data) {
+      return this.onLoadDataSuccess(this.data);
+    }
+    switch (this.get('format')) {
+    case 'json':
+      this.loadJSON();
+      break;
+    case 'csv':
+      this.loadCSV();
+      break;
+    default:
+      console.error(this + ".load() Unknown Data Format!");
+      this.onLoadDataError(null, 'Unknown Data Format!', new Error('Unknown Data Format!'));
+    }
+    return this;
+  },
+  loadJSON: function(){
+    var _this = this;
+    $.ajax({
+      url: this.get('url'),
+      dataType: 'json',
+      success: function(data){
+        return _this.onLoadDataSuccess(new TimeSeriesData(data));
+      },
+      error: this.onLoadDataError
+    });
+    return this;
+  },
+  loadCSV: function(){
+    var _this = this;
+    $.ajax({
+      url: this.get('url'),
+      dataType: 'text',
+      success: function(data){
+        return _this.onLoadDataSuccess(new CSVData(data));
+      },
+      error: this.onLoadDataError
+    });
+    return this;
+  },
+  onLoadDataSuccess: function(data){
+    this.data = data;
+    this.unwait();
+    this.trigger('load-data-success', this);
+    return this.triggerReady();
+  },
+  onLoadDataError: function(jqXHR, txtStatus, err){
+    console.error(this + " Error loading data! -- " + msg + ": " + (err || ''));
+    this.unwait();
+    this._errorLoading = true;
+    return this.trigger('load-data-error', this, txtStatus, err);
+  },
+  getDateColumn: function(){
+    var _ref;
+    return (_ref = this.data) != null ? _ref.dateColumn : void 8;
+  },
+  getData: function(){
+    var _ref;
+    return ((_ref = this.data) != null ? typeof _ref.toJSON == 'function' ? _ref.toJSON() : void 8 : void 8) || this.data;
+  },
+  getColumn: function(idx){
+    var _ref;
+    return (_ref = this.data) != null ? _ref.columns[idx] : void 8;
+  },
+  getColumnName: function(idx){
+    var _ref, _ref2;
+    return (_ref = this.get('metrics')) != null ? (_ref2 = _ref[idx]) != null ? _ref2.label : void 8 : void 8;
+  },
+  getColumnIndex: function(name){
+    var that;
+    if (that = _.find(this.get('metrics'), function(it){
+      return it.label === name;
+    })) {
+      return that.idx;
+    }
+    return -1;
+  },
+  onMetricChange: function(){
+    return this.metrics.reset(this.get('metrics'));
+  }
+});
+/**
+ * @class
+ */
+DataSourceList = exports.DataSourceList = BaseList.extend({
+  urlRoot: '/datasources',
+  model: DataSource,
+  constructor: (function(){
+    function DataSourceList(){
+      return BaseList.apply(this, arguments);
+    }
+    return DataSourceList;
+  }()),
+  initialize: function(){
+    return BaseList.prototype.initialize.apply(this, arguments);
+  }
+});
+ALL_SOURCES = new DataSourceList;
+sourceCache = new ModelCache(DataSource, {
+  ready: false,
+  cache: ALL_SOURCES
+});
+$.getJSON('/datasources/all', function(data){
+  ALL_SOURCES.reset(_.map(data, op.I));
+  return sourceCache.triggerReady();
+});
+DataSource.getAllSources = function(){
+  return ALL_SOURCES;
+};
\ No newline at end of file
diff --git a/lib/data/datasource-model.mod.js b/lib/data/datasource-model.mod.js
new file mode 100644 (file)
index 0000000..49b9f7e
--- /dev/null
@@ -0,0 +1,221 @@
+require.define('/node_modules/kraken/data/datasource-model.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var op, TimeSeriesData, CSVData, BaseModel, BaseList, ModelCache, Metric, MetricList, DataSource, DataSourceList, ALL_SOURCES, sourceCache, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/util/timeseries'), TimeSeriesData = _ref.TimeSeriesData, CSVData = _ref.CSVData;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, ModelCache = _ref.ModelCache;
+_ref = require('kraken/data/metric-model'), Metric = _ref.Metric, MetricList = _ref.MetricList;
+/**
+ * @class
+ */
+DataSource = exports.DataSource = BaseModel.extend({
+  __bind__: ['onLoadDataSuccess', 'onLoadDataError'],
+  urlRoot: '/datasources',
+  ready: false
+  /**
+   * Parsed data for this datasource.
+   * @type Array
+   */,
+  data: null,
+  defaults: function(){
+    return {
+      id: '',
+      url: '',
+      format: 'json',
+      name: '',
+      shortName: '',
+      title: '',
+      subtitle: '',
+      desc: '',
+      notes: '',
+      timespan: {
+        start: null,
+        end: null,
+        step: '1mo'
+      },
+      columns: [],
+      chart: {
+        chartType: 'dygraphs',
+        options: {}
+      }
+    };
+  },
+  url: function(){
+    return "/datasources/" + this.id + ".json";
+  },
+  constructor: (function(){
+    function DataSource(){
+      return BaseModel.apply(this, arguments);
+    }
+    return DataSource;
+  }()),
+  initialize: function(){
+    this.attributes = this.canonicalize(this.attributes);
+    BaseModel.prototype.initialize.apply(this, arguments);
+    this.constructor.register(this);
+    this.metrics = new MetricList(this.attributes.metrics);
+    return this.on('change:metrics', this.onMetricChange, this);
+  },
+  canonicalize: 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 {
+          col.type || (col.type = 'int');
+          return col;
+        }
+      });
+    } else {
+      ds.metrics = _.map(cols.labels, function(label, idx){
+        return {
+          idx: idx,
+          label: label,
+          type: cols.types[idx] || 'int'
+        };
+      });
+    }
+    return ds;
+  },
+  loadAll: function(){
+    this.loader({
+      start: function(){
+        var _this = this;
+        return Seq().seq_(function(next){
+          _this.once('fetch-success', next.ok);
+          return _this.loadModel();
+        }).seq_(function(next){
+          _this.once('load-data-success', next.ok);
+          return _this.loadData();
+        }).seq(function(){
+          return _this.trigger('load-success', _this);
+        });
+      }
+    });
+    return this;
+  },
+  loadData: function(){
+    this.wait();
+    this.trigger('load-data', this);
+    if (this.data) {
+      return this.onLoadDataSuccess(this.data);
+    }
+    switch (this.get('format')) {
+    case 'json':
+      this.loadJSON();
+      break;
+    case 'csv':
+      this.loadCSV();
+      break;
+    default:
+      console.error(this + ".load() Unknown Data Format!");
+      this.onLoadDataError(null, 'Unknown Data Format!', new Error('Unknown Data Format!'));
+    }
+    return this;
+  },
+  loadJSON: function(){
+    var _this = this;
+    $.ajax({
+      url: this.get('url'),
+      dataType: 'json',
+      success: function(data){
+        return _this.onLoadDataSuccess(new TimeSeriesData(data));
+      },
+      error: this.onLoadDataError
+    });
+    return this;
+  },
+  loadCSV: function(){
+    var _this = this;
+    $.ajax({
+      url: this.get('url'),
+      dataType: 'text',
+      success: function(data){
+        return _this.onLoadDataSuccess(new CSVData(data));
+      },
+      error: this.onLoadDataError
+    });
+    return this;
+  },
+  onLoadDataSuccess: function(data){
+    this.data = data;
+    this.unwait();
+    this.trigger('load-data-success', this);
+    return this.triggerReady();
+  },
+  onLoadDataError: function(jqXHR, txtStatus, err){
+    console.error(this + " Error loading data! -- " + msg + ": " + (err || ''));
+    this.unwait();
+    this._errorLoading = true;
+    return this.trigger('load-data-error', this, txtStatus, err);
+  },
+  getDateColumn: function(){
+    var _ref;
+    return (_ref = this.data) != null ? _ref.dateColumn : void 8;
+  },
+  getData: function(){
+    var _ref;
+    return ((_ref = this.data) != null ? typeof _ref.toJSON == 'function' ? _ref.toJSON() : void 8 : void 8) || this.data;
+  },
+  getColumn: function(idx){
+    var _ref;
+    return (_ref = this.data) != null ? _ref.columns[idx] : void 8;
+  },
+  getColumnName: function(idx){
+    var _ref, _ref2;
+    return (_ref = this.get('metrics')) != null ? (_ref2 = _ref[idx]) != null ? _ref2.label : void 8 : void 8;
+  },
+  getColumnIndex: function(name){
+    var that;
+    if (that = _.find(this.get('metrics'), function(it){
+      return it.label === name;
+    })) {
+      return that.idx;
+    }
+    return -1;
+  },
+  onMetricChange: function(){
+    return this.metrics.reset(this.get('metrics'));
+  }
+});
+/**
+ * @class
+ */
+DataSourceList = exports.DataSourceList = BaseList.extend({
+  urlRoot: '/datasources',
+  model: DataSource,
+  constructor: (function(){
+    function DataSourceList(){
+      return BaseList.apply(this, arguments);
+    }
+    return DataSourceList;
+  }()),
+  initialize: function(){
+    return BaseList.prototype.initialize.apply(this, arguments);
+  }
+});
+ALL_SOURCES = new DataSourceList;
+sourceCache = new ModelCache(DataSource, {
+  ready: false,
+  cache: ALL_SOURCES
+});
+$.getJSON('/datasources/all', function(data){
+  ALL_SOURCES.reset(_.map(data, op.I));
+  return sourceCache.triggerReady();
+});
+DataSource.getAllSources = function(){
+  return ALL_SOURCES;
+};
+
+});
diff --git a/lib/data/datasource-ui-view.js b/lib/data/datasource-ui-view.js
new file mode 100644 (file)
index 0000000..0f2b4c7
--- /dev/null
@@ -0,0 +1,76 @@
+var op, BaseModel, BaseList, BaseView, DataSourceUIView, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView;
+/**
+ * @class
+ * Model is a Metric.
+ */
+DataSourceUIView = exports.DataSourceUIView = BaseView.extend({
+  __bind__: [],
+  tagName: 'section',
+  className: 'datasource-ui',
+  template: require('kraken/template/data/datasource-ui'),
+  events: {
+    'click .datasource-summary': 'onHeaderClick',
+    'click .datasource-source-metric': 'onSelectMetric'
+  },
+  graph_id: null,
+  dataset: null,
+  datasources: null,
+  constructor: (function(){
+    function DataSourceUIView(){
+      return BaseView.apply(this, arguments);
+    }
+    return DataSourceUIView;
+  }()),
+  initialize: function(){
+    var _ref;
+    this.graph_id = (_ref = this.options).graph_id;
+    this.dataset = _ref.dataset;
+    this.datasources = _ref.datasources;
+    return BaseView.prototype.initialize.apply(this, arguments);
+  },
+  toTemplateLocals: function(){
+    var locals, ds, hasSource, hasMetric, dsts, ts, hasTimespan;
+    locals = this.model.toJSON();
+    locals.graph_id = this.graph_id;
+    locals.dataset = this.dataset;
+    locals.datasources = this.datasources;
+    locals.cid = this.model.cid;
+    ds = this.model.source;
+    hasSource = this.model.get('source_id') != null && ds;
+    locals.source_summary = !hasSource
+      ? '<Select Source>'
+      : ds.get('shortName');
+    hasMetric = hasSource && this.model.get('source_col') != null;
+    locals.metric_summary = !hasMetric
+      ? '<Select Metric>'
+      : this.model.getSourceColumnName();
+    dsts = (ds != null ? ds.get('timespan') : void 8) || {};
+    ts = locals.timespan = _.defaults(_.clone(this.model.get('timespan')), dsts);
+    hasTimespan = hasMetric && ts.start && ts.end && ts.step;
+    locals.timespan_summary = !hasTimespan
+      ? '<Select Timespan>'
+      : ts.start + " &mdash; " + ts.end;
+    return locals;
+  },
+  onHeaderClick: function(){
+    return this.$el.toggleClass('in');
+  },
+  onSelectMetric: function(evt){
+    var el, source_id, source_col, _ref;
+    el = $(evt.currentTarget);
+    _ref = el.data(), source_id = _ref.source_id, source_col = _ref.source_col;
+    source_col = parseInt(source_col);
+    if (!source_id || isNaN(source_col)) {
+      return;
+    }
+    this.$('.source-metrics .datasource-source-metric').removeClass('active');
+    el.addClass('active');
+    this.model.set({
+      source_col: source_col,
+      source_id: source_id
+    });
+    return this.trigger('metric-change', this.model, this);
+  }
+});
\ No newline at end of file
diff --git a/lib/data/datasource-ui-view.mod.js b/lib/data/datasource-ui-view.mod.js
new file mode 100644 (file)
index 0000000..b9714f7
--- /dev/null
@@ -0,0 +1,80 @@
+require.define('/node_modules/kraken/data/datasource-ui-view.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var op, BaseModel, BaseList, BaseView, DataSourceUIView, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView;
+/**
+ * @class
+ * Model is a Metric.
+ */
+DataSourceUIView = exports.DataSourceUIView = BaseView.extend({
+  __bind__: [],
+  tagName: 'section',
+  className: 'datasource-ui',
+  template: require('kraken/template/data/datasource-ui'),
+  events: {
+    'click .datasource-summary': 'onHeaderClick',
+    'click .datasource-source-metric': 'onSelectMetric'
+  },
+  graph_id: null,
+  dataset: null,
+  datasources: null,
+  constructor: (function(){
+    function DataSourceUIView(){
+      return BaseView.apply(this, arguments);
+    }
+    return DataSourceUIView;
+  }()),
+  initialize: function(){
+    var _ref;
+    this.graph_id = (_ref = this.options).graph_id;
+    this.dataset = _ref.dataset;
+    this.datasources = _ref.datasources;
+    return BaseView.prototype.initialize.apply(this, arguments);
+  },
+  toTemplateLocals: function(){
+    var locals, ds, hasSource, hasMetric, dsts, ts, hasTimespan;
+    locals = this.model.toJSON();
+    locals.graph_id = this.graph_id;
+    locals.dataset = this.dataset;
+    locals.datasources = this.datasources;
+    locals.cid = this.model.cid;
+    ds = this.model.source;
+    hasSource = this.model.get('source_id') != null && ds;
+    locals.source_summary = !hasSource
+      ? '<Select Source>'
+      : ds.get('shortName');
+    hasMetric = hasSource && this.model.get('source_col') != null;
+    locals.metric_summary = !hasMetric
+      ? '<Select Metric>'
+      : this.model.getSourceColumnName();
+    dsts = (ds != null ? ds.get('timespan') : void 8) || {};
+    ts = locals.timespan = _.defaults(_.clone(this.model.get('timespan')), dsts);
+    hasTimespan = hasMetric && ts.start && ts.end && ts.step;
+    locals.timespan_summary = !hasTimespan
+      ? '<Select Timespan>'
+      : ts.start + " &mdash; " + ts.end;
+    return locals;
+  },
+  onHeaderClick: function(){
+    return this.$el.toggleClass('in');
+  },
+  onSelectMetric: function(evt){
+    var el, source_id, source_col, _ref;
+    el = $(evt.currentTarget);
+    _ref = el.data(), source_id = _ref.source_id, source_col = _ref.source_col;
+    source_col = parseInt(source_col);
+    if (!source_id || isNaN(source_col)) {
+      return;
+    }
+    this.$('.source-metrics .datasource-source-metric').removeClass('active');
+    el.addClass('active');
+    this.model.set({
+      source_col: source_col,
+      source_id: source_id
+    });
+    return this.trigger('metric-change', this.model, this);
+  }
+});
+
+});
diff --git a/lib/data/datasource-view.js b/lib/data/datasource-view.js
new file mode 100644 (file)
index 0000000..3ec50e1
--- /dev/null
@@ -0,0 +1,21 @@
+var op, BaseModel, BaseList, BaseView, DataSourceView, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView;
+/**
+ * @class
+ */
+DataSourceView = exports.DataSourceView = BaseView.extend({
+  __bind__: [],
+  tagName: 'section',
+  className: 'datasource',
+  template: require('kraken/template/data/datasource'),
+  constructor: (function(){
+    function DataSourceView(){
+      return BaseView.apply(this, arguments);
+    }
+    return DataSourceView;
+  }()),
+  initialize: function(){
+    return BaseView.prototype.initialize.apply(this, arguments);
+  }
+});
\ No newline at end of file
diff --git a/lib/data/datasource-view.mod.js b/lib/data/datasource-view.mod.js
new file mode 100644 (file)
index 0000000..7f9c205
--- /dev/null
@@ -0,0 +1,25 @@
+require.define('/node_modules/kraken/data/datasource-view.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var op, BaseModel, BaseList, BaseView, DataSourceView, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView;
+/**
+ * @class
+ */
+DataSourceView = exports.DataSourceView = BaseView.extend({
+  __bind__: [],
+  tagName: 'section',
+  className: 'datasource',
+  template: require('kraken/template/data/datasource'),
+  constructor: (function(){
+    function DataSourceView(){
+      return BaseView.apply(this, arguments);
+    }
+    return DataSourceView;
+  }()),
+  initialize: function(){
+    return BaseView.prototype.initialize.apply(this, arguments);
+  }
+});
+
+});
diff --git a/lib/data/index.js b/lib/data/index.js
new file mode 100644 (file)
index 0000000..55cb2d2
--- /dev/null
@@ -0,0 +1,15 @@
+var metric_model, metric_edit_view, datasource_model, datasource_view, datasource_ui_view, dataset_model, dataset_view, data_view;
+metric_model = require('kraken/data/metric-model');
+metric_edit_view = require('kraken/data/metric-edit-view');
+datasource_model = require('kraken/data/datasource-model');
+datasource_view = require('kraken/data/datasource-view');
+datasource_ui_view = require('kraken/data/datasource-ui-view');
+dataset_model = require('kraken/data/dataset-model');
+dataset_view = require('kraken/data/dataset-view');
+data_view = require('kraken/data/data-view');
+__import(__import(__import(__import(__import(__import(__import(__import(exports, datasource_model), datasource_view), datasource_ui_view), dataset_model), dataset_view), metric_model), metric_edit_view), data_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/data/index.mod.js b/lib/data/index.mod.js
new file mode 100644 (file)
index 0000000..ba4ef66
--- /dev/null
@@ -0,0 +1,19 @@
+require.define('/node_modules/kraken/data/index.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var metric_model, metric_edit_view, datasource_model, datasource_view, datasource_ui_view, dataset_model, dataset_view, data_view;
+metric_model = require('kraken/data/metric-model');
+metric_edit_view = require('kraken/data/metric-edit-view');
+datasource_model = require('kraken/data/datasource-model');
+datasource_view = require('kraken/data/datasource-view');
+datasource_ui_view = require('kraken/data/datasource-ui-view');
+dataset_model = require('kraken/data/dataset-model');
+dataset_view = require('kraken/data/dataset-view');
+data_view = require('kraken/data/data-view');
+__import(__import(__import(__import(__import(__import(__import(__import(exports, datasource_model), datasource_view), datasource_ui_view), dataset_model), dataset_view), metric_model), metric_edit_view), data_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/data/metric-edit-view.js b/lib/data/metric-edit-view.js
new file mode 100644 (file)
index 0000000..15f7c78
--- /dev/null
@@ -0,0 +1,81 @@
+var op, BaseView, Metric, DataSourceUIView, MetricEditView, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+BaseView = require('kraken/base').BaseView;
+Metric = require('kraken/data/metric-model').Metric;
+DataSourceUIView = require('kraken/data/datasource-ui-view').DataSourceUIView;
+/**
+ * @class
+ * Model is a Metric.
+ */
+MetricEditView = exports.MetricEditView = BaseView.extend({
+  __bind__: ['onChange'],
+  tagName: 'section',
+  className: 'metric-edit-ui',
+  template: require('kraken/template/data/metric-edit'),
+  callOnReturnKeypress: 'onChange',
+  events: {
+    'keydown .metric-label': 'onReturnKeypress'
+  },
+  graph_id: null,
+  dataset: null,
+  datasources: null,
+  datasource_ui_view: null,
+  constructor: (function(){
+    function MetricEditView(){
+      return BaseView.apply(this, arguments);
+    }
+    return MetricEditView;
+  }()),
+  initialize: function(){
+    var _ref, _this = this;
+    this.graph_id = (_ref = this.options).graph_id;
+    this.dataset = _ref.dataset;
+    this.datasources = _ref.datasources;
+    this.model || (this.model = new Metric);
+    BaseView.prototype.initialize.apply(this, arguments);
+    this.on('attach', this.onAttach, this);
+    this.datasource_ui_view = new DataSourceUIView({
+      model: this.model,
+      graph_id: this.graph_id,
+      dataset: this.dataset,
+      datasources: this.datasources
+    });
+    return this.addSubview(this.datasource_ui_view).on('metric-update', function(){
+      return _this.trigger('update', _this);
+    }).on('metric-change', this.onSourceMetricChange, this);
+  },
+  toTemplateLocals: function(){
+    var locals;
+    locals = MetricEditView.__super__.toTemplateLocals.apply(this, arguments);
+    return locals.graph_id = this.graph_id, locals.dataset = this.dataset, locals.datasources = this.datasources, locals.placeholder_label = this.model.getPlaceholderLabel(), locals;
+  },
+  update: function(){
+    MetricEditView.__super__.update.apply(this, arguments);
+    this.$('.metric-label').attr('placeholder', this.model.getPlaceholderLabel());
+    this.$('.color-swatch').data('color', this.model.getColor()).colorpicker('update');
+    return this;
+  },
+  onAttach: function(){
+    return this.$('.color-swatch').data('color', this.model.get('color')).colorpicker().on('hide', this.onChange);
+  },
+  onChange: function(evt){
+    var attrs, same;
+    attrs = this.$('form.metric-edit-form').formData();
+    same = _.isEqual(this.model.attributes, attrs);
+    console.log(this + ".onChange! (same? " + same + ")");
+    _.dump(this.model.attributes, 'old', !same);
+    _.dump(attrs, 'new', !same);
+    if (!_.isEqual(this.model.attributes, attrs)) {
+      this.model.set(attrs, {
+        silent: true
+      });
+      return this.trigger('metric-update', this);
+    }
+  },
+  onSourceMetricChange: function(metric){
+    console.log(this + ".onSourceMetricChange!", metric);
+    this.$('.metric-label').attr('placeholder', this.model.getPlaceholderLabel());
+    this.trigger('metric-change', this.model, this);
+    return this;
+  }
+});
\ No newline at end of file
diff --git a/lib/data/metric-edit-view.mod.js b/lib/data/metric-edit-view.mod.js
new file mode 100644 (file)
index 0000000..40d5442
--- /dev/null
@@ -0,0 +1,85 @@
+require.define('/node_modules/kraken/data/metric-edit-view.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var op, BaseView, Metric, DataSourceUIView, MetricEditView, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+BaseView = require('kraken/base').BaseView;
+Metric = require('kraken/data/metric-model').Metric;
+DataSourceUIView = require('kraken/data/datasource-ui-view').DataSourceUIView;
+/**
+ * @class
+ * Model is a Metric.
+ */
+MetricEditView = exports.MetricEditView = BaseView.extend({
+  __bind__: ['onChange'],
+  tagName: 'section',
+  className: 'metric-edit-ui',
+  template: require('kraken/template/data/metric-edit'),
+  callOnReturnKeypress: 'onChange',
+  events: {
+    'keydown .metric-label': 'onReturnKeypress'
+  },
+  graph_id: null,
+  dataset: null,
+  datasources: null,
+  datasource_ui_view: null,
+  constructor: (function(){
+    function MetricEditView(){
+      return BaseView.apply(this, arguments);
+    }
+    return MetricEditView;
+  }()),
+  initialize: function(){
+    var _ref, _this = this;
+    this.graph_id = (_ref = this.options).graph_id;
+    this.dataset = _ref.dataset;
+    this.datasources = _ref.datasources;
+    this.model || (this.model = new Metric);
+    BaseView.prototype.initialize.apply(this, arguments);
+    this.on('attach', this.onAttach, this);
+    this.datasource_ui_view = new DataSourceUIView({
+      model: this.model,
+      graph_id: this.graph_id,
+      dataset: this.dataset,
+      datasources: this.datasources
+    });
+    return this.addSubview(this.datasource_ui_view).on('metric-update', function(){
+      return _this.trigger('update', _this);
+    }).on('metric-change', this.onSourceMetricChange, this);
+  },
+  toTemplateLocals: function(){
+    var locals;
+    locals = MetricEditView.__super__.toTemplateLocals.apply(this, arguments);
+    return locals.graph_id = this.graph_id, locals.dataset = this.dataset, locals.datasources = this.datasources, locals.placeholder_label = this.model.getPlaceholderLabel(), locals;
+  },
+  update: function(){
+    MetricEditView.__super__.update.apply(this, arguments);
+    this.$('.metric-label').attr('placeholder', this.model.getPlaceholderLabel());
+    this.$('.color-swatch').data('color', this.model.getColor()).colorpicker('update');
+    return this;
+  },
+  onAttach: function(){
+    return this.$('.color-swatch').data('color', this.model.get('color')).colorpicker().on('hide', this.onChange);
+  },
+  onChange: function(evt){
+    var attrs, same;
+    attrs = this.$('form.metric-edit-form').formData();
+    same = _.isEqual(this.model.attributes, attrs);
+    console.log(this + ".onChange! (same? " + same + ")");
+    _.dump(this.model.attributes, 'old', !same);
+    _.dump(attrs, 'new', !same);
+    if (!_.isEqual(this.model.attributes, attrs)) {
+      this.model.set(attrs, {
+        silent: true
+      });
+      return this.trigger('metric-update', this);
+    }
+  },
+  onSourceMetricChange: function(metric){
+    console.log(this + ".onSourceMetricChange!", metric);
+    this.$('.metric-label').attr('placeholder', this.model.getPlaceholderLabel());
+    this.trigger('metric-change', this.model, this);
+    return this;
+  }
+});
+
+});
diff --git a/lib/data/metric-model.js b/lib/data/metric-model.js
new file mode 100644 (file)
index 0000000..c304e53
--- /dev/null
@@ -0,0 +1,180 @@
+var op, BaseModel, BaseList, ProjectColors, DataSourceList, DataSource, Metric, MetricList, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList;
+ProjectColors = require('kraken/data/project-colors');
+DataSource = DataSourceList = null;
+/**
+ * @class
+ */
+Metric = exports.Metric = BaseModel.extend({
+  NEW_METRIC_LABEL: 'New Metric',
+  urlRoot: '/metrics'
+  /**
+   * Data source of the Metric.
+   * @type DataSource
+   */,
+  source: null,
+  is_def_label: true,
+  defaults: function(){
+    return {
+      index: 0,
+      label: '',
+      type: 'int',
+      timespan: {
+        start: null,
+        end: null,
+        step: null
+      },
+      disabled: false,
+      source_id: null,
+      source_col: -1,
+      color: null,
+      visible: true,
+      format_value: null,
+      format_axis: null,
+      transforms: [],
+      scale: 1.0,
+      chartType: null
+    };
+  },
+  constructor: (function(){
+    function Metric(){
+      return BaseModel.apply(this, arguments);
+    }
+    return Metric;
+  }()),
+  initialize: function(){
+    BaseModel.prototype.initialize.apply(this, arguments);
+    this.is_def_label = this.isDefaultLabel();
+    this.on('change:source_id', this.load, this);
+    this.on('change:source_col', this.updateId, this);
+    this.on('change:label', this.updateLabel, this);
+    return this.load();
+  },
+  getDateColumn: function(){
+    var _ref;
+    return (_ref = this.source) != null ? _ref.getDateColumn() : void 8;
+  },
+  getData: function(){
+    var _ref;
+    return (_ref = this.source) != null ? _ref.getColumn(this.get('source_col')) : void 8;
+  },
+  getLabel: function(){
+    return this.get('label') || this.getPlaceholderLabel();
+  },
+  getPlaceholderLabel: function(){
+    var col, name;
+    col = this.get('source_col');
+    if (this.source && col >= 0) {
+      name = this.source.get('shortName') + ", " + this.source.getColumnName(col);
+    }
+    return name || this.NEW_METRIC_LABEL;
+  },
+  getSourceColumnName: function(){
+    var col;
+    col = this.get('source_col');
+    if (this.source && col > 0) {
+      return this.source.getColumnName(col);
+    }
+  },
+  getColor: function(){
+    return this.get('color') || ProjectColors.lookup(this.get('label')) || 'black';
+  },
+  load: function(opts){
+    var source_id, _ref, _this = this;
+    opts == null && (opts = {});
+    source_id = this.get('source_id');
+    if (opts.force || ((_ref = this.source) != null ? _ref.id : void 8) !== source_id) {
+      this.resetReady();
+    }
+    if (this.loading || this.ready) {
+      return this;
+    }
+    if (!(source_id && this.get('source_col') >= 0)) {
+      return this.triggerReady();
+    }
+    this.updateId();
+    this.loading = true;
+    this.wait();
+    this.trigger('load', this);
+    DataSource.lookup(source_id, function(err, source){
+      _this.loading = false;
+      _this.unwait();
+      if (err) {
+        return console.error(_this + " Error loading DataSource! " + err);
+      } else {
+        _this.source = source;
+        _this.is_def_label = _this.isDefaultLabel();
+        _this.updateId();
+        return _this.triggerReady();
+      }
+    });
+    return this;
+  },
+  isDefaultLabel: function(){
+    var label;
+    label = this.get('label');
+    return !label || label === this.getPlaceholderLabel() || label === this.NEW_METRIC_LABEL;
+  },
+  updateLabel: function(){
+    var label;
+    if (!this.source) {
+      return this;
+    }
+    label = this.get('label');
+    if (!label || this.is_def_label) {
+      this.set('label', '');
+      this.is_def_label = true;
+    } else {
+      this.is_def_label = this.isDefaultLabel();
+    }
+    return this;
+  },
+  updateId: function(){
+    var source_id, source_col;
+    source_id = this.get('source_id');
+    source_col = this.get('source_col');
+    if (source_id && source_col != null) {
+      this.id = source_id + "[" + source_col + "]";
+    }
+    this.updateLabel();
+    return this;
+  }
+  /**
+   * Check whether the metric has aiight-looking values so we don't
+   * attempt to graph unconfigured crap.
+   */,
+  isOk: function(){
+    var _ref;
+    return (_ref = this.source) != null ? _ref.ready : void 8;
+  }
+});
+/**
+ * @class
+ */
+MetricList = exports.MetricList = BaseList.extend({
+  urlRoot: '/metrics',
+  model: Metric,
+  constructor: (function(){
+    function MetricList(){
+      return BaseList.apply(this, arguments);
+    }
+    return MetricList;
+  }()),
+  initialize: function(){
+    return BaseList.prototype.initialize.apply(this, arguments);
+  },
+  comparator: function(metric){
+    var _ref;
+    return (_ref = metric.get('index')) != null ? _ref : Infinity;
+  },
+  onlyOk: function(){
+    return new MetricList(this.filter(function(it){
+      return it.isOk();
+    }));
+  }
+});
+setTimeout(function(){
+  var _ref;
+  return _ref = require('kraken/data/datasource-model'), DataSource = _ref.DataSource, DataSourceList = _ref.DataSourceList, _ref;
+}, 10);
\ No newline at end of file
diff --git a/lib/data/metric-model.mod.js b/lib/data/metric-model.mod.js
new file mode 100644 (file)
index 0000000..831825b
--- /dev/null
@@ -0,0 +1,184 @@
+require.define('/node_modules/kraken/data/metric-model.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var op, BaseModel, BaseList, ProjectColors, DataSourceList, DataSource, Metric, MetricList, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList;
+ProjectColors = require('kraken/data/project-colors');
+DataSource = DataSourceList = null;
+/**
+ * @class
+ */
+Metric = exports.Metric = BaseModel.extend({
+  NEW_METRIC_LABEL: 'New Metric',
+  urlRoot: '/metrics'
+  /**
+   * Data source of the Metric.
+   * @type DataSource
+   */,
+  source: null,
+  is_def_label: true,
+  defaults: function(){
+    return {
+      index: 0,
+      label: '',
+      type: 'int',
+      timespan: {
+        start: null,
+        end: null,
+        step: null
+      },
+      disabled: false,
+      source_id: null,
+      source_col: -1,
+      color: null,
+      visible: true,
+      format_value: null,
+      format_axis: null,
+      transforms: [],
+      scale: 1.0,
+      chartType: null
+    };
+  },
+  constructor: (function(){
+    function Metric(){
+      return BaseModel.apply(this, arguments);
+    }
+    return Metric;
+  }()),
+  initialize: function(){
+    BaseModel.prototype.initialize.apply(this, arguments);
+    this.is_def_label = this.isDefaultLabel();
+    this.on('change:source_id', this.load, this);
+    this.on('change:source_col', this.updateId, this);
+    this.on('change:label', this.updateLabel, this);
+    return this.load();
+  },
+  getDateColumn: function(){
+    var _ref;
+    return (_ref = this.source) != null ? _ref.getDateColumn() : void 8;
+  },
+  getData: function(){
+    var _ref;
+    return (_ref = this.source) != null ? _ref.getColumn(this.get('source_col')) : void 8;
+  },
+  getLabel: function(){
+    return this.get('label') || this.getPlaceholderLabel();
+  },
+  getPlaceholderLabel: function(){
+    var col, name;
+    col = this.get('source_col');
+    if (this.source && col >= 0) {
+      name = this.source.get('shortName') + ", " + this.source.getColumnName(col);
+    }
+    return name || this.NEW_METRIC_LABEL;
+  },
+  getSourceColumnName: function(){
+    var col;
+    col = this.get('source_col');
+    if (this.source && col > 0) {
+      return this.source.getColumnName(col);
+    }
+  },
+  getColor: function(){
+    return this.get('color') || ProjectColors.lookup(this.get('label')) || 'black';
+  },
+  load: function(opts){
+    var source_id, _ref, _this = this;
+    opts == null && (opts = {});
+    source_id = this.get('source_id');
+    if (opts.force || ((_ref = this.source) != null ? _ref.id : void 8) !== source_id) {
+      this.resetReady();
+    }
+    if (this.loading || this.ready) {
+      return this;
+    }
+    if (!(source_id && this.get('source_col') >= 0)) {
+      return this.triggerReady();
+    }
+    this.updateId();
+    this.loading = true;
+    this.wait();
+    this.trigger('load', this);
+    DataSource.lookup(source_id, function(err, source){
+      _this.loading = false;
+      _this.unwait();
+      if (err) {
+        return console.error(_this + " Error loading DataSource! " + err);
+      } else {
+        _this.source = source;
+        _this.is_def_label = _this.isDefaultLabel();
+        _this.updateId();
+        return _this.triggerReady();
+      }
+    });
+    return this;
+  },
+  isDefaultLabel: function(){
+    var label;
+    label = this.get('label');
+    return !label || label === this.getPlaceholderLabel() || label === this.NEW_METRIC_LABEL;
+  },
+  updateLabel: function(){
+    var label;
+    if (!this.source) {
+      return this;
+    }
+    label = this.get('label');
+    if (!label || this.is_def_label) {
+      this.set('label', '');
+      this.is_def_label = true;
+    } else {
+      this.is_def_label = this.isDefaultLabel();
+    }
+    return this;
+  },
+  updateId: function(){
+    var source_id, source_col;
+    source_id = this.get('source_id');
+    source_col = this.get('source_col');
+    if (source_id && source_col != null) {
+      this.id = source_id + "[" + source_col + "]";
+    }
+    this.updateLabel();
+    return this;
+  }
+  /**
+   * Check whether the metric has aiight-looking values so we don't
+   * attempt to graph unconfigured crap.
+   */,
+  isOk: function(){
+    var _ref;
+    return (_ref = this.source) != null ? _ref.ready : void 8;
+  }
+});
+/**
+ * @class
+ */
+MetricList = exports.MetricList = BaseList.extend({
+  urlRoot: '/metrics',
+  model: Metric,
+  constructor: (function(){
+    function MetricList(){
+      return BaseList.apply(this, arguments);
+    }
+    return MetricList;
+  }()),
+  initialize: function(){
+    return BaseList.prototype.initialize.apply(this, arguments);
+  },
+  comparator: function(metric){
+    var _ref;
+    return (_ref = metric.get('index')) != null ? _ref : Infinity;
+  },
+  onlyOk: function(){
+    return new MetricList(this.filter(function(it){
+      return it.isOk();
+    }));
+  }
+});
+setTimeout(function(){
+  var _ref;
+  return _ref = require('kraken/data/datasource-model'), DataSource = _ref.DataSource, DataSourceList = _ref.DataSourceList, _ref;
+}, 10);
+
+});
diff --git a/lib/data/project-colors.js b/lib/data/project-colors.js
new file mode 100644 (file)
index 0000000..eab642f
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * @fileOverview Applies consistent coloring to language/project Metrics with a null `color` field.
+ */
+var PROJECT_COLORS, project, color, PROJECT_TESTS, lookupColor, _res;
+PROJECT_COLORS = exports.PROJECT_COLORS = {
+  'target': '#cccccc',
+  'total': '#182B53',
+  'all projects': '#182B53',
+  'world': '#182B53',
+  'commons': '#d73027',
+  'north america': '#4596FF',
+  'english': '#4596FF',
+  'asia pacific': '#83BB32',
+  'japanese': '#83BB32',
+  'china': '#AD3238',
+  'chinese': '#AD3238',
+  'europe': '#FF0097',
+  'german': '#FF0097',
+  'dutch': '#EF8158',
+  'french': '#1A9380',
+  'italian': '#FF87FF',
+  'portuguese': '#B64926',
+  'swedish': '#5DD2A4',
+  'russian': '#FA0000',
+  'latin america': '#FFB719',
+  'spanish': '#FFB719',
+  'middle east': '#00675B',
+  'india': '#553DC9'
+};
+_res = [];
+for (project in PROJECT_COLORS) {
+  color = PROJECT_COLORS[project];
+  _res.push({
+    pat: RegExp('\\b' + project.replace(/ /g, '[ _-]') + '\\b', 'i'),
+    project: project,
+    color: color
+  });
+}
+PROJECT_TESTS = _res;
+lookupColor = exports.lookup = function(label){
+  var project, pat, color, _ref, _ref2;
+  for (project in _ref = PROJECT_TESTS) {
+    _ref2 = _ref[project], pat = _ref2.pat, color = _ref2.color;
+    if (pat.test(label)) {
+      return color;
+    }
+  }
+};
\ No newline at end of file
diff --git a/lib/data/project-colors.mod.js b/lib/data/project-colors.mod.js
new file mode 100644 (file)
index 0000000..674a936
--- /dev/null
@@ -0,0 +1,52 @@
+require.define('/node_modules/kraken/data/project-colors.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+/**
+ * @fileOverview Applies consistent coloring to language/project Metrics with a null `color` field.
+ */
+var PROJECT_COLORS, project, color, PROJECT_TESTS, lookupColor, _res;
+PROJECT_COLORS = exports.PROJECT_COLORS = {
+  'target': '#cccccc',
+  'total': '#182B53',
+  'all projects': '#182B53',
+  'world': '#182B53',
+  'commons': '#d73027',
+  'north america': '#4596FF',
+  'english': '#4596FF',
+  'asia pacific': '#83BB32',
+  'japanese': '#83BB32',
+  'china': '#AD3238',
+  'chinese': '#AD3238',
+  'europe': '#FF0097',
+  'german': '#FF0097',
+  'dutch': '#EF8158',
+  'french': '#1A9380',
+  'italian': '#FF87FF',
+  'portuguese': '#B64926',
+  'swedish': '#5DD2A4',
+  'russian': '#FA0000',
+  'latin america': '#FFB719',
+  'spanish': '#FFB719',
+  'middle east': '#00675B',
+  'india': '#553DC9'
+};
+_res = [];
+for (project in PROJECT_COLORS) {
+  color = PROJECT_COLORS[project];
+  _res.push({
+    pat: RegExp('\\b' + project.replace(/ /g, '[ _-]') + '\\b', 'i'),
+    project: project,
+    color: color
+  });
+}
+PROJECT_TESTS = _res;
+lookupColor = exports.lookup = function(label){
+  var project, pat, color, _ref, _ref2;
+  for (project in _ref = PROJECT_TESTS) {
+    _ref2 = _ref[project], pat = _ref2.pat, color = _ref2.color;
+    if (pat.test(label)) {
+      return color;
+    }
+  }
+};
+
+});
diff --git a/lib/graph/graph-display-view.js b/lib/graph/graph-display-view.js
new file mode 100644 (file)
index 0000000..ed4246f
--- /dev/null
@@ -0,0 +1,67 @@
+var moment, op, Graph, GraphView, root, GraphDisplayView, _ref, _;
+moment = require('moment');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+Graph = require('kraken/graph/graph-model').Graph;
+GraphView = require('kraken/graph/graph-view').GraphView;
+root = function(){
+  return this;
+}();
+/**
+ * @class View for a graph visualization encapsulating.
+ */
+GraphDisplayView = exports.GraphDisplayView = GraphView.extend({
+  tagName: 'section',
+  className: 'graph graph-display',
+  template: require('kraken/template/graph/graph-display'),
+  events: {
+    'focus      .graph-permalink input': 'onPermalinkFocus',
+    'click      .export-button': 'exportChart'
+  },
+  constructor: (function(){
+    function GraphDisplayView(){
+      return BaseView.apply(this, arguments);
+    }
+    return GraphDisplayView;
+  }()),
+  initialize: function(o){
+    o == null && (o = {});
+    this.data = {};
+    GraphDisplayView.__super__.initialize.apply(this, arguments);
+    this.chartOptions(this.model.getOptions(), {
+      silent: true
+    });
+    return this.loadData();
+  },
+  render: function(){
+    if (!(this.ready && !this.isRendering)) {
+      return this;
+    }
+    this.wait();
+    this.checkWaiting();
+    root.title = this.get('name') + " | Limn";
+    GraphDisplayView.__super__.render.apply(this, arguments);
+    this.unwait();
+    this.checkWaiting();
+    this.isRendering = false;
+    return this;
+  }
+  /**
+   * Exports graph as png
+   */,
+  exportChart: function(evt){
+    var img;
+    console.log(this + ".export!");
+    img = this.$el.find('.export-image');
+    Dygraph.Export.asPNG(this.chart, img);
+    return window.open(img.src, "toDataURL() image");
+  }
+  /**
+   * Selects the graph permalink input field.
+   */,
+  onPermalinkFocus: function(evt){
+    var _this = this;
+    return _.defer(function(){
+      return _this.$('.graph-permalink input').select();
+    });
+  }
+});
\ No newline at end of file
diff --git a/lib/graph/graph-display-view.mod.js b/lib/graph/graph-display-view.mod.js
new file mode 100644 (file)
index 0000000..76919e9
--- /dev/null
@@ -0,0 +1,71 @@
+require.define('/node_modules/kraken/graph/graph-display-view.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var moment, op, Graph, GraphView, root, GraphDisplayView, _ref, _;
+moment = require('moment');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+Graph = require('kraken/graph/graph-model').Graph;
+GraphView = require('kraken/graph/graph-view').GraphView;
+root = function(){
+  return this;
+}();
+/**
+ * @class View for a graph visualization encapsulating.
+ */
+GraphDisplayView = exports.GraphDisplayView = GraphView.extend({
+  tagName: 'section',
+  className: 'graph graph-display',
+  template: require('kraken/template/graph/graph-display'),
+  events: {
+    'focus      .graph-permalink input': 'onPermalinkFocus',
+    'click      .export-button': 'exportChart'
+  },
+  constructor: (function(){
+    function GraphDisplayView(){
+      return BaseView.apply(this, arguments);
+    }
+    return GraphDisplayView;
+  }()),
+  initialize: function(o){
+    o == null && (o = {});
+    this.data = {};
+    GraphDisplayView.__super__.initialize.apply(this, arguments);
+    this.chartOptions(this.model.getOptions(), {
+      silent: true
+    });
+    return this.loadData();
+  },
+  render: function(){
+    if (!(this.ready && !this.isRendering)) {
+      return this;
+    }
+    this.wait();
+    this.checkWaiting();
+    root.title = this.get('name') + " | Limn";
+    GraphDisplayView.__super__.render.apply(this, arguments);
+    this.unwait();
+    this.checkWaiting();
+    this.isRendering = false;
+    return this;
+  }
+  /**
+   * Exports graph as png
+   */,
+  exportChart: function(evt){
+    var img;
+    console.log(this + ".export!");
+    img = this.$el.find('.export-image');
+    Dygraph.Export.asPNG(this.chart, img);
+    return window.open(img.src, "toDataURL() image");
+  }
+  /**
+   * Selects the graph permalink input field.
+   */,
+  onPermalinkFocus: function(evt){
+    var _this = this;
+    return _.defer(function(){
+      return _this.$('.graph-permalink input').select();
+    });
+  }
+});
+
+});
diff --git a/lib/graph/graph-edit-view.js b/lib/graph/graph-edit-view.js
new file mode 100644 (file)
index 0000000..2a562eb
--- /dev/null
@@ -0,0 +1,194 @@
+var moment, Graph, GraphView, ChartOptionScaffold, DEBOUNCE_RENDER, DataView, DataSetView, DataSet, root, GraphEditView, _, _ref;
+moment = require('moment');
+_ = require('kraken/util/underscore');
+Graph = require('kraken/graph/graph-model').Graph;
+GraphView = require('kraken/graph/graph-view').GraphView;
+_ref = require('kraken/chart'), ChartOptionScaffold = _ref.ChartOptionScaffold, DEBOUNCE_RENDER = _ref.DEBOUNCE_RENDER;
+_ref = require('kraken/data'), DataView = _ref.DataView, DataSetView = _ref.DataSetView, DataSet = _ref.DataSet;
+root = function(){
+  return this;
+}();
+/**
+ * @class View for a graph visualization encapsulating the editing UI for:
+ * - Graph metadata, such as name, description, slug
+ * - Chart options, using ChartOptionScaffold
+ */
+GraphEditView = exports.GraphEditView = GraphView.extend({
+  __bind__: ['wait', 'unwait', 'onChartTypeReady', 'onScaffoldChange', 'onFirstClickRenderOptionsTab', 'onFirstClickRenderDataTab'],
+  className: 'graph-edit graph',
+  template: require('kraken/template/graph/graph-edit'),
+  events: {
+    'click    .redraw-button': 'stopAndRender',
+    'click    .load-button': 'load',
+    'click    .save-button': 'save',
+    'click    .done-button': 'done',
+    'keypress .graph-name': 'onNameKeypress',
+    'keypress .graph-details input[type="text"]': 'onKeypress',
+    'keypress .chart-options .value': 'onKeypress',
+    'submit   form.graph-details': 'onDetailsSubmit',
+    'change   :not(.chart-options) select': 'onDetailsSubmit',
+    'submit   form.chart-options': 'onOptionsSubmit',
+    'change   .chart-options input[type="checkbox"]': 'onOptionsSubmit'
+  },
+  constructor: (function(){
+    function GraphEditView(){
+      return BaseView.apply(this, arguments);
+    }
+    return GraphEditView;
+  }()),
+  initialize: function(o){
+    o == null && (o = {});
+    GraphEditView.__super__.initialize.apply(this, arguments);
+    this.wait();
+    this.scaffold = this.addSubview(new ChartOptionScaffold);
+    this.data_view = this.addSubview(new DataView({
+      model: this.model.dataset,
+      graph_id: this.id
+    }));
+    this.data_view.on('start-waiting', this.wait, this).on('stop-waiting', this.unwait, this).on('metric-change', this.onDataChange, this);
+    this.$el.on('click', '.graph-data-tab', this.onFirstClickRenderDataTab);
+    this.$el.on('click', '.graph-options-tab', this.onFirstClickRenderOptionsTab);
+    return this.loadData();
+  },
+  onChartTypeReady: function(){
+    this.scaffold.collection.reset(this.model.chartType.options_ordered);
+    this.scaffold.on('change', this.onScaffoldChange);
+    return this.chartOptions(this.model.getOptions(), {
+      silent: true
+    });
+  },
+  onReady: function(){
+    if (this.ready) {
+      return;
+    }
+    this.unwait();
+    this.model.chartType.on('ready', this.onChartTypeReady);
+    this.triggerReady();
+    this.scaffold.triggerReady();
+    this.chartOptions(this.model.getOptions(), {
+      silent: true
+    });
+    this.render();
+    this.model.dataset.metrics.on('add remove change', this.render, this);
+    this.model.on('metric-data-loaded', this.render, this);
+    return _.delay(this.checkWaiting, 50);
+  }
+  /**
+   * Save the graph and return to the graph viewer/browser.
+   */,
+  done: function(){
+    return this.save();
+  }
+  /**
+   * Flush all changes.
+   */,
+  change: function(){
+    this.model.change();
+    this.scaffold.invoke('change');
+    return this;
+  },
+  chartOptions: function(values, opts){
+    var k, v, fields, options, _ref, _i, _len;
+    if (arguments.length > 1 && typeof values === 'string') {
+      k = arguments[0], v = arguments[1], opts = arguments[2];
+      values = (_ref = {}, _ref[k + ""] = v, _ref);
+    }
+    fields = this.scaffold.collection;
+    if (values) {
+      for (k in values) {
+        v = values[k];
+        if ((_ref = fields.get(k)) != null) {
+          _ref.setValue(v, opts);
+        }
+      }
+      return this;
+    } else {
+      options = this.model.getOptions({
+        keepDefaults: false,
+        keepUnchanged: true
+      });
+      for (_i = 0, _len = (_ref = this.FILTER_CHART_OPTIONS).length; _i < _len; ++_i) {
+        k = _ref[_i];
+        if (k in options && !options[k]) {
+          delete options[k];
+        }
+      }
+      return options;
+    }
+  },
+  attachSubviews: function(){
+    GraphEditView.__super__.attachSubviews.apply(this, arguments);
+    return this.checkWaiting();
+  },
+  render: function(){
+    if (!(this.ready && !this.isRendering)) {
+      return this;
+    }
+    this.wait();
+    this.checkWaiting();
+    root.title = this.get('name') + " | Limn";
+    GraphEditView.__super__.render.apply(this, arguments);
+    this.unwait();
+    this.isRendering = false;
+    return this;
+  }
+  /**
+   * Update the page URL using HTML5 History API
+   */,
+  updateURL: function(){
+    var json, title, url;
+    json = this.toJSON();
+    title = (this.model.get('name') || 'New Graph') + " | Edit Graph | Limn";
+    url = this.toURL('edit');
+    return History.pushState(json, title, url);
+  },
+  onScaffoldChange: function(scaffold, value, key, field){
+    var current;
+    current = this.model.getOption(key);
+    if (!(_.isEqual(value, current) || (current === void 8 && field.isDefault()))) {
+      return this.model.setOption(key, value, {
+        silent: true
+      });
+    }
+  },
+  onDataChange: function(){
+    console.log(this + ".onDataChange!");
+    return this.model.once('data-ready', this.render, this).loadData({
+      force: true
+    });
+  },
+  onFirstClickRenderOptionsTab: function(){
+    this.$el.off('click', '.graph-options-tab', this.onFirstClickRenderOptionsTab);
+    return this.scaffold.render();
+  },
+  onFirstClickRenderDataTab: function(){
+    var _this = this;
+    this.$el.off('click', '.graph-data-tab', this.onFirstClickRenderDataTab);
+    return _.defer(function(){
+      return _this.data_view.onMetricsChanged();
+    });
+  },
+  onKeypress: function(evt){
+    if (evt.keyCode === 13) {
+      return $(evt.target).submit();
+    }
+  },
+  onNameKeypress: function(evt){
+    if (evt.keyCode === 13) {
+      return this.$('form.graph-details').submit();
+    }
+  },
+  onDetailsSubmit: function(){
+    var details;
+    console.log(this + ".onDetailsSubmit!");
+    this.$('form.graph-details .graph-name').val(this.$('.graph-name-row .graph-name').val());
+    details = this.$('form.graph-details').formData();
+    this.model.set(details);
+    return false;
+  },
+  onOptionsSubmit: function(){
+    console.log(this + ".onOptionsSubmit!");
+    this.render();
+    return false;
+  }
+});
\ No newline at end of file
diff --git a/lib/graph/graph-edit-view.mod.js b/lib/graph/graph-edit-view.mod.js
new file mode 100644 (file)
index 0000000..ed45d38
--- /dev/null
@@ -0,0 +1,198 @@
+require.define('/node_modules/kraken/graph/graph-edit-view.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var moment, Graph, GraphView, ChartOptionScaffold, DEBOUNCE_RENDER, DataView, DataSetView, DataSet, root, GraphEditView, _, _ref;
+moment = require('moment');
+_ = require('kraken/util/underscore');
+Graph = require('kraken/graph/graph-model').Graph;
+GraphView = require('kraken/graph/graph-view').GraphView;
+_ref = require('kraken/chart'), ChartOptionScaffold = _ref.ChartOptionScaffold, DEBOUNCE_RENDER = _ref.DEBOUNCE_RENDER;
+_ref = require('kraken/data'), DataView = _ref.DataView, DataSetView = _ref.DataSetView, DataSet = _ref.DataSet;
+root = function(){
+  return this;
+}();
+/**
+ * @class View for a graph visualization encapsulating the editing UI for:
+ * - Graph metadata, such as name, description, slug
+ * - Chart options, using ChartOptionScaffold
+ */
+GraphEditView = exports.GraphEditView = GraphView.extend({
+  __bind__: ['wait', 'unwait', 'onChartTypeReady', 'onScaffoldChange', 'onFirstClickRenderOptionsTab', 'onFirstClickRenderDataTab'],
+  className: 'graph-edit graph',
+  template: require('kraken/template/graph/graph-edit'),
+  events: {
+    'click    .redraw-button': 'stopAndRender',
+    'click    .load-button': 'load',
+    'click    .save-button': 'save',
+    'click    .done-button': 'done',
+    'keypress .graph-name': 'onNameKeypress',
+    'keypress .graph-details input[type="text"]': 'onKeypress',
+    'keypress .chart-options .value': 'onKeypress',
+    'submit   form.graph-details': 'onDetailsSubmit',
+    'change   :not(.chart-options) select': 'onDetailsSubmit',
+    'submit   form.chart-options': 'onOptionsSubmit',
+    'change   .chart-options input[type="checkbox"]': 'onOptionsSubmit'
+  },
+  constructor: (function(){
+    function GraphEditView(){
+      return BaseView.apply(this, arguments);
+    }
+    return GraphEditView;
+  }()),
+  initialize: function(o){
+    o == null && (o = {});
+    GraphEditView.__super__.initialize.apply(this, arguments);
+    this.wait();
+    this.scaffold = this.addSubview(new ChartOptionScaffold);
+    this.data_view = this.addSubview(new DataView({
+      model: this.model.dataset,
+      graph_id: this.id
+    }));
+    this.data_view.on('start-waiting', this.wait, this).on('stop-waiting', this.unwait, this).on('metric-change', this.onDataChange, this);
+    this.$el.on('click', '.graph-data-tab', this.onFirstClickRenderDataTab);
+    this.$el.on('click', '.graph-options-tab', this.onFirstClickRenderOptionsTab);
+    return this.loadData();
+  },
+  onChartTypeReady: function(){
+    this.scaffold.collection.reset(this.model.chartType.options_ordered);
+    this.scaffold.on('change', this.onScaffoldChange);
+    return this.chartOptions(this.model.getOptions(), {
+      silent: true
+    });
+  },
+  onReady: function(){
+    if (this.ready) {
+      return;
+    }
+    this.unwait();
+    this.model.chartType.on('ready', this.onChartTypeReady);
+    this.triggerReady();
+    this.scaffold.triggerReady();
+    this.chartOptions(this.model.getOptions(), {
+      silent: true
+    });
+    this.render();
+    this.model.dataset.metrics.on('add remove change', this.render, this);
+    this.model.on('metric-data-loaded', this.render, this);
+    return _.delay(this.checkWaiting, 50);
+  }
+  /**
+   * Save the graph and return to the graph viewer/browser.
+   */,
+  done: function(){
+    return this.save();
+  }
+  /**
+   * Flush all changes.
+   */,
+  change: function(){
+    this.model.change();
+    this.scaffold.invoke('change');
+    return this;
+  },
+  chartOptions: function(values, opts){
+    var k, v, fields, options, _ref, _i, _len;
+    if (arguments.length > 1 && typeof values === 'string') {
+      k = arguments[0], v = arguments[1], opts = arguments[2];
+      values = (_ref = {}, _ref[k + ""] = v, _ref);
+    }
+    fields = this.scaffold.collection;
+    if (values) {
+      for (k in values) {
+        v = values[k];
+        if ((_ref = fields.get(k)) != null) {
+          _ref.setValue(v, opts);
+        }
+      }
+      return this;
+    } else {
+      options = this.model.getOptions({
+        keepDefaults: false,
+        keepUnchanged: true
+      });
+      for (_i = 0, _len = (_ref = this.FILTER_CHART_OPTIONS).length; _i < _len; ++_i) {
+        k = _ref[_i];
+        if (k in options && !options[k]) {
+          delete options[k];
+        }
+      }
+      return options;
+    }
+  },
+  attachSubviews: function(){
+    GraphEditView.__super__.attachSubviews.apply(this, arguments);
+    return this.checkWaiting();
+  },
+  render: function(){
+    if (!(this.ready && !this.isRendering)) {
+      return this;
+    }
+    this.wait();
+    this.checkWaiting();
+    root.title = this.get('name') + " | Limn";
+    GraphEditView.__super__.render.apply(this, arguments);
+    this.unwait();
+    this.isRendering = false;
+    return this;
+  }
+  /**
+   * Update the page URL using HTML5 History API
+   */,
+  updateURL: function(){
+    var json, title, url;
+    json = this.toJSON();
+    title = (this.model.get('name') || 'New Graph') + " | Edit Graph | Limn";
+    url = this.toURL('edit');
+    return History.pushState(json, title, url);
+  },
+  onScaffoldChange: function(scaffold, value, key, field){
+    var current;
+    current = this.model.getOption(key);
+    if (!(_.isEqual(value, current) || (current === void 8 && field.isDefault()))) {
+      return this.model.setOption(key, value, {
+        silent: true
+      });
+    }
+  },
+  onDataChange: function(){
+    console.log(this + ".onDataChange!");
+    return this.model.once('data-ready', this.render, this).loadData({
+      force: true
+    });
+  },
+  onFirstClickRenderOptionsTab: function(){
+    this.$el.off('click', '.graph-options-tab', this.onFirstClickRenderOptionsTab);
+    return this.scaffold.render();
+  },
+  onFirstClickRenderDataTab: function(){
+    var _this = this;
+    this.$el.off('click', '.graph-data-tab', this.onFirstClickRenderDataTab);
+    return _.defer(function(){
+      return _this.data_view.onMetricsChanged();
+    });
+  },
+  onKeypress: function(evt){
+    if (evt.keyCode === 13) {
+      return $(evt.target).submit();
+    }
+  },
+  onNameKeypress: function(evt){
+    if (evt.keyCode === 13) {
+      return this.$('form.graph-details').submit();
+    }
+  },
+  onDetailsSubmit: function(){
+    var details;
+    console.log(this + ".onDetailsSubmit!");
+    this.$('form.graph-details .graph-name').val(this.$('.graph-name-row .graph-name').val());
+    details = this.$('form.graph-details').formData();
+    this.model.set(details);
+    return false;
+  },
+  onOptionsSubmit: function(){
+    console.log(this + ".onOptionsSubmit!");
+    this.render();
+    return false;
+  }
+});
+
+});
diff --git a/lib/graph/graph-list-view.js b/lib/graph/graph-list-view.js
new file mode 100644 (file)
index 0000000..03f37e4
--- /dev/null
@@ -0,0 +1,30 @@
+var op, BaseView, Graph, GraphList, root, DEBOUNCE_RENDER, GraphListView, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+BaseView = require('kraken/base').BaseView;
+_ref = require('kraken/graph/graph-model'), Graph = _ref.Graph, GraphList = _ref.GraphList;
+root = function(){
+  return this;
+}();
+DEBOUNCE_RENDER = 100;
+/**
+ * @class View for a showing a list of all saved graphs
+ */
+GraphListView = exports.GraphListView = BaseView.extend({
+  __bind__: ['render'],
+  __debounce__: ['render'],
+  tagName: 'section',
+  className: 'graph-list-view',
+  template: require('kraken/template/graph/graph-list'),
+  data: {},
+  ready: false,
+  initialize: function(){
+    this.model = this.collection || (this.collection = new GraphList);
+    return BaseView.prototype.initialize.apply(this, arguments);
+  },
+  toTemplateLocals: function(){
+    var locals;
+    locals = BaseView.prototype.toTemplateLocals.apply(this, arguments);
+    locals.collection = this.collection;
+    return locals;
+  }
+});
\ No newline at end of file
diff --git a/lib/graph/graph-list-view.mod.js b/lib/graph/graph-list-view.mod.js
new file mode 100644 (file)
index 0000000..b943a49
--- /dev/null
@@ -0,0 +1,34 @@
+require.define('/node_modules/kraken/graph/graph-list-view.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var op, BaseView, Graph, GraphList, root, DEBOUNCE_RENDER, GraphListView, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+BaseView = require('kraken/base').BaseView;
+_ref = require('kraken/graph/graph-model'), Graph = _ref.Graph, GraphList = _ref.GraphList;
+root = function(){
+  return this;
+}();
+DEBOUNCE_RENDER = 100;
+/**
+ * @class View for a showing a list of all saved graphs
+ */
+GraphListView = exports.GraphListView = BaseView.extend({
+  __bind__: ['render'],
+  __debounce__: ['render'],
+  tagName: 'section',
+  className: 'graph-list-view',
+  template: require('kraken/template/graph/graph-list'),
+  data: {},
+  ready: false,
+  initialize: function(){
+    this.model = this.collection || (this.collection = new GraphList);
+    return BaseView.prototype.initialize.apply(this, arguments);
+  },
+  toTemplateLocals: function(){
+    var locals;
+    locals = BaseView.prototype.toTemplateLocals.apply(this, arguments);
+    locals.collection = this.collection;
+    return locals;
+  }
+});
+
+});
diff --git a/lib/graph/graph-model.js b/lib/graph/graph-model.js
new file mode 100644 (file)
index 0000000..cf04dbd
--- /dev/null
@@ -0,0 +1,447 @@
+var Seq, Cascade, BaseModel, BaseList, ModelCache, ChartType, DataSet, root, Graph, GraphList, _ref, _;
+Seq = require('seq');
+_ref = require('kraken/util'), _ = _ref._, Cascade = _ref.Cascade;
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, ModelCache = _ref.ModelCache;
+ChartType = require('kraken/chart').ChartType;
+DataSet = require('kraken/data').DataSet;
+root = function(){
+  return this;
+}();
+/**
+ * Represents a Graph, including its charting options, dataset, annotations, and all
+ * other settings for both its content and presentation.
+ */
+Graph = exports.Graph = BaseModel.extend({
+  IGNORE_OPTIONS: ['width', 'height', 'timingName'],
+  urlRoot: '/graphs'
+  /**
+   * Whether this Graph has loaded all assets, parent-graphs, and related
+   * resources.
+   * @type Boolean
+   */,
+  ready: false
+  /**
+   * Whether this Graph has loaded the actual data needed to draw the chart.
+   * @type Boolean
+   */,
+  dataReady: false
+  /**
+   * The chart type backing this graph.
+   * @type ChartType
+   */,
+  chartType: null
+  /**
+   * List of graph parents.
+   * @type GraphList
+   */,
+  parents: null
+  /**
+   * Cascade of objects for options lookup (includes own options).
+   * @type Cascade
+   * @private
+   */,
+  optionCascade: null
+  /**
+   * Attribute defaults.
+   */,
+  defaults: function(){
+    return {
+      slug: '',
+      name: '',
+      desc: '',
+      notes: '',
+      width: 'auto',
+      height: 320,
+      parents: ['root'],
+      data: {
+        palette: null,
+        metrics: [],
+        lines: []
+      },
+      callout: {
+        enabled: true,
+        metric_idx: 0,
+        label: ''
+      },
+      chartType: 'dygraphs',
+      options: {}
+    };
+  },
+  url: function(){
+    return this.urlRoot + "/" + this.get('slug') + ".json";
+  },
+  constructor: (function(){
+    function Graph(attributes, opts){
+      attributes == null && (attributes = {});
+      attributes.options || (attributes.options = {});
+      if (attributes.id != null) {
+        attributes.slug || (attributes.slug = attributes.id);
+      }
+      this.optionCascade = new Cascade(attributes.options);
+      return BaseModel.call(this, attributes, opts);
+    }
+    return Graph;
+  }()),
+  initialize: function(attributes){
+    var _this = this;
+    BaseModel.prototype.initialize.apply(this, arguments);
+    this.constructor.register(this);
+    this.parents = new GraphList;
+    this.chartType = ChartType.create(this);
+    this.on('change:chartType', function(){
+      return _this.chartType = ChartType.create(_this);
+    });
+    this.dataset = new DataSet((__import({
+      id: this.id
+    }, this.get('data')))).on('change', this.onDataSetChange, this).on('metric-data-loaded', function(dataset, metric){
+      return _this.trigger('metric-data-loaded', _this, metric);
+    });
+    this.set('data', this.dataset, {
+      silent: true
+    });
+    return this.trigger('init', this);
+  },
+  load: function(opts){
+    var _this = this;
+    opts == null && (opts = {});
+    if ((this.loading || this.ready) && !opts.force) {
+      return this;
+    }
+    this.loading = true;
+    this.wait();
+    this.trigger('load', this);
+    Seq().seq_(function(next){
+      if (_this.isNew()) {
+        return next.ok();
+      }
+      _this.wait();
+      return _this.fetch({
+        error: _this.unwaitAnd(function(err){
+          console.error(_this + ".fetch() --> error! " + arguments);
+          return next.ok();
+        }),
+        success: _this.unwaitAnd(function(model, res){
+          _this.dataset.set(_this.get('data'));
+          _this.set('data', _this.dataset, {
+            silent: true
+          });
+          return next.ok(res);
+        })
+      });
+    }).seq_(function(next){
+      return next.ok(_this.get('parents'));
+    }).flatten().seqMap_(function(next, parent_id){
+      _this.wait();
+      return Graph.lookup(parent_id, next);
+    }).seqEach_(function(next, parent){
+      _this.parents.add(parent);
+      _this.optionCascade.addLookup(parent.get('options'));
+      _this.unwait();
+      return next.ok();
+    }).seq_(function(next){
+      return _this.dataset.once('ready', next.ok).load();
+    }).seq(function(){
+      _this.loading = false;
+      _this.unwait();
+      return _this.triggerReady();
+    });
+    return this;
+  },
+  loadData: function(opts){
+    var _this = this;
+    opts == null && (opts = {});
+    if (opts.force) {
+      this.resetReady('dataReady', 'data-ready');
+    }
+    if (this.loading || this.dataReady) {
+      return this;
+    }
+    if (!this.dataset.metrics.length) {
+      return this.triggerReady('dataReady', 'data-ready');
+    }
+    this.wait();
+    this.loading = true;
+    this.trigger('load-data', this);
+    Seq(this.dataset.metrics.models).parEach_(function(next, metric){
+      return metric.once('ready', next.ok).load();
+    }).parEach_(function(next, metric){
+      if (!metric.source) {
+        console.warn(_this + ".loadData() -- Skipping metric " + metric + " with invalid source!", metric);
+        return next.ok();
+      }
+      return metric.source.once('load-data-success', next.ok).loadData();
+    }).seq(function(){
+      _this.loading = false;
+      _this.unwait();
+      return _this.triggerReady('dataReady', 'data-ready');
+    });
+    return this;
+  },
+  getData: function(){
+    return this.dataset.getData();
+  },
+  onDataSetChange: function(){
+    return this.trigger('change', this, this.dataset, 'data');
+  },
+  get: function(key){
+    if (_.startsWith(key, 'options.')) {
+      return this.getOption(key.slice(8));
+    } else {
+      return Graph.__super__.get.call(this, key);
+    }
+  },
+  set: function(key, value, opts){
+    var values, setter, options, _ref;
+    if (_.isObject(key) && key != null) {
+      _ref = [key, value], values = _ref[0], opts = _ref[1];
+    } else {
+      values = (_ref = {}, _ref[key + ""] = value, _ref);
+    }
+    values = this.parse(values);
+    setter = Graph.__super__.set;
+    if (values.options) {
+      options = values.options, delete values.options;
+      if (!this.attributes.options) {
+        setter.call(this, {
+          options: options
+        }, {
+          silent: true
+        });
+      }
+      this.setOption(options, opts);
+    }
+    return setter.call(this, values, opts);
+  },
+  getCalloutData: function(){
+    var m, data, dates, len, i, v, last, latest, last_month, last_year, callout;
+    if (!((m = this.dataset.metrics.at(0)) && (data = m.getData()) && (dates = m.getDateColumn()))) {
+      return;
+    }
+    len = Math.min(data.length, dates.length);
+    if (data.length < len) {
+      data = data.slice(data.length - len);
+    }
+    if (dates.length < len) {
+      dates = dates.slice(dates.length - len);
+    }
+    for (i = 0; i < len; ++i) {
+      v = data[i];
+      if (v != null && !isNaN(v)) {
+        break;
+      }
+    }
+    if (i > 0) {
+      data = data.slice(i);
+      dates = dates.slice(i);
+    }
+    last = len - 1;
+    for (i = 0; i < len; ++i) {
+      v = data[last - i];
+      if (v != null && !isNaN(v)) {
+        break;
+      }
+    }
+    if (i > 0) {
+      data = data.slice(0, last - (i - 1));
+      dates = dates.slice(0, last - (i - 1));
+    }
+    latest = data.length - 1;
+    last_month = latest - 1;
+    last_year = latest - 12;
+    return callout = {
+      latest: data[latest],
+      month: {
+        dates: [dates[last_month], dates[latest]],
+        value: [data[last_month], data[latest], data[latest] - data[last_month]]
+      },
+      year: {
+        dates: [dates[last_year], dates[latest]],
+        value: [data[last_year], data[latest], data[latest] - data[last_year]]
+      }
+    };
+  },
+  hasOption: function(key){
+    return this.getOption(key) === void 8;
+  },
+  getOption: function(key, def){
+    return this.optionCascade.get(key, def);
+  },
+  setOption: function(key, value, opts){
+    var values, options, changed, _ref;
+    opts == null && (opts = {});
+    if (_.isObject(key) && key != null) {
+      _ref = [key, value || {}], values = _ref[0], opts = _ref[1];
+    } else {
+      values = (_ref = {}, _ref[key + ""] = value, _ref);
+    }
+    options = this.get('options');
+    changed = false;
+    for (key in values) {
+      value = values[key];
+      if (_.contains(this.IGNORE_OPTIONS, key)) {
+        continue;
+      }
+      changed = true;
+      _.setNested(options, key, value, {
+        ensure: true
+      });
+      if (!opts.silent) {
+        this.trigger("change:options." + key, this, value, key, opts);
+      }
+    }
+    if (changed && !opts.silent) {
+      this.trigger("change:options", this, options, 'options', opts);
+      this.trigger("change", this, options, 'options', opts);
+    }
+    return this;
+  },
+  unsetOption: function(key, opts){
+    var options;
+    opts == null && (opts = {});
+    if (!(this.optionCascade.unset(key) === void 8 || opts.silent)) {
+      options = this.get('options');
+      this.trigger("change:options." + key, this, void 8, key, opts);
+      this.trigger("change:options", this, options, 'options', opts);
+      this.trigger("change", this, options, 'options', opts);
+    }
+    return this;
+  },
+  inheritOption: function(key, opts){
+    var old, options;
+    opts == null && (opts = {});
+    old = this.getOption(key);
+    this.optionCascade.inherit(key);
+    if (!(this.getOption(key) === old || opts.silent)) {
+      options = this.get('options');
+      this.trigger("change:options." + key, this, void 8, key, opts);
+      this.trigger("change:options", this, options, 'options', opts);
+      this.trigger("change:options", this, options, 'options', opts);
+    }
+    return this;
+  },
+  getOptions: function(opts){
+    var options, k, v;
+    opts == null && (opts = {});
+    opts = __import({
+      keepDefaults: true,
+      keepUnchanged: true
+    }, opts);
+    options = this.optionCascade.collapse();
+    for (k in options) {
+      v = options[k];
+      if (v === void 8 || (!opts.keepDefaults && this.isDefaultOption(k)) || (!opts.keepUnchanged && !this.isChangedOption(k))) {
+        delete options[k];
+      }
+    }
+    return options;
+  },
+  parse: function(data){
+    var k, v;
+    if (typeof data === 'string') {
+      data = JSON.parse(data);
+    }
+    for (k in data) {
+      v = data[k];
+      if (v !== 'auto' && _.contains(['width', 'height'], k)) {
+        data[k] = Number(v);
+      }
+    }
+    return data;
+  }
+  /**
+   * @returns {Boolean} Whether the value for option `k` is inherited or not.
+   */,
+  isOwnOption: function(k){
+    return this.optionCascade.isOwnValue(k);
+  }
+  /**
+   * @returns {Boolean} Whether the value for option `k` is the graph default or not.
+   */,
+  isDefaultOption: function(k){
+    return this.chartType.isDefault(k, this.getOption(k));
+  }
+  /**
+   * Whether the value for option `k` differs from that of its parent graphs.
+   * @returns {Boolean}
+   */,
+  isChangedOption: function(k){
+    return this.optionCascade.isModifiedValue(k) && !this.isDefaultOption(k);
+  },
+  toJSON: function(opts){
+    var json, _ref;
+    opts == null && (opts = {});
+    opts = __import({
+      keepDefaults: true,
+      keepUnchanged: true
+    }, opts);
+    return json = (_ref = _.clone(this.attributes), _ref.options = this.getOptions(opts), _ref);
+  },
+  toKVPairs: function(opts){
+    var kvo, k, v, _ref;
+    opts == null && (opts = {});