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 = {});
+    opts = __import({
+      keepSlug: false,
+      keepDefaults: false,
+      keepUnchanged: false
+    }, opts);
+    kvo = this.toJSON(opts);
+    kvo.parents = JSON.stringify(kvo.parents);
+    if (!opts.keepSlug) {
+      delete kvo.slug;
+    }
+    delete kvo.data;
+    for (k in _ref = kvo.options) {
+      v = _ref[k];
+      kvo.options[k] = this.serialize(v);
+    }
+    return _.collapseObject(kvo);
+  },
+  toKV: function(opts){
+    return _.toKV(this.toKVPairs(opts));
+  }
+  /**
+   * @returns {String} URL identifying this model.
+   */,
+  toURL: function(action){
+    var slug, path;
+    slug = this.get('slug');
+    path = _.compact([this.urlRoot, slug, action]).join('/');
+    return path + "?" + this.toKV({
+      keepSlug: !!slug
+    });
+  }
+  /**
+   * @returns {String} Path portion of slug URL, e.g.  /graphs/:slug
+   */,
+  toLink: function(){
+    return this.urlRoot + "/" + this.get('slug');
+  }
+  /**
+   * @returns {String} Permalinked URI, e.g. http://reportcard.wmflabs.org/:slug
+   */,
+  toPermalink: function(){
+    return root.location.protocol + "//" + window.location.host + this.toLink();
+  }
+});
+new ModelCache(Graph);
+GraphList = exports.GraphList = BaseList.extend({
+  urlRoot: '/graphs',
+  model: Graph,
+  constructor: (function(){
+    function GraphList(){
+      return BaseList.apply(this, arguments);
+    }
+    return GraphList;
+  }()),
+  initialize: function(){
+    return BaseList.prototype.initialize.apply(this, arguments);
+  },
+  toString: function(){
+    return this.toStringWithIds();
+  }
+});
+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/graph/graph-model.mod.js b/lib/graph/graph-model.mod.js
new file mode 100644 (file)
index 0000000..3953385
--- /dev/null
@@ -0,0 +1,451 @@
+require.define('/node_modules/kraken/graph/graph-model.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+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 = {});
+    opts = __import({
+      keepSlug: false,
+      keepDefaults: false,
+      keepUnchanged: false
+    }, opts);
+    kvo = this.toJSON(opts);
+    kvo.parents = JSON.stringify(kvo.parents);
+    if (!opts.keepSlug) {
+      delete kvo.slug;
+    }
+    delete kvo.data;
+    for (k in _ref = kvo.options) {
+      v = _ref[k];
+      kvo.options[k] = this.serialize(v);
+    }
+    return _.collapseObject(kvo);
+  },
+  toKV: function(opts){
+    return _.toKV(this.toKVPairs(opts));
+  }
+  /**
+   * @returns {String} URL identifying this model.
+   */,
+  toURL: function(action){
+    var slug, path;
+    slug = this.get('slug');
+    path = _.compact([this.urlRoot, slug, action]).join('/');
+    return path + "?" + this.toKV({
+      keepSlug: !!slug
+    });
+  }
+  /**
+   * @returns {String} Path portion of slug URL, e.g.  /graphs/:slug
+   */,
+  toLink: function(){
+    return this.urlRoot + "/" + this.get('slug');
+  }
+  /**
+   * @returns {String} Permalinked URI, e.g. http://reportcard.wmflabs.org/:slug
+   */,
+  toPermalink: function(){
+    return root.location.protocol + "//" + window.location.host + this.toLink();
+  }
+});
+new ModelCache(Graph);
+GraphList = exports.GraphList = BaseList.extend({
+  urlRoot: '/graphs',
+  model: Graph,
+  constructor: (function(){
+    function GraphList(){
+      return BaseList.apply(this, arguments);
+    }
+    return GraphList;
+  }()),
+  initialize: function(){
+    return BaseList.prototype.initialize.apply(this, arguments);
+  },
+  toString: function(){
+    return this.toStringWithIds();
+  }
+});
+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/graph/graph-view.js b/lib/graph/graph-view.js
new file mode 100644 (file)
index 0000000..7c98892
--- /dev/null
@@ -0,0 +1,311 @@
+var moment, BaseView, Graph, root, DEBOUNCE_RENDER, GraphView, _;
+moment = require('moment');
+_ = require('kraken/util/underscore');
+BaseView = require('kraken/base').BaseView;
+Graph = require('kraken/graph/graph-model').Graph;
+root = function(){
+  return this;
+}();
+DEBOUNCE_RENDER = 100;
+/**
+ * @class Base view for a Graph visualizations.
+ */
+GraphView = exports.GraphView = BaseView.extend({
+  FILTER_CHART_OPTIONS: ['file', 'labels', 'visibility', 'colors', 'dateWindow', 'ticker', 'timingName', 'xValueParser', 'axisLabelFormatter', 'xAxisLabelFormatter', 'yAxisLabelFormatter', 'valueFormatter', 'xValueFormatter', 'yValueFormatter'],
+  __bind__: ['render', 'stopAndRender', 'resizeViewport', 'checkWaiting', 'onReady', 'onSync', 'onModelChange'],
+  __debounce__: ['render'],
+  tagName: 'section'
+  /**
+   * The chart type backing this graph.
+   * @type ChartType
+   */,
+  chartType: null,
+  constructor: (function(){
+    function GraphView(){
+      return BaseView.apply(this, arguments);
+    }
+    return GraphView;
+  }()),
+  initialize: function(o){
+    var name, _i, _ref, _len;
+    o == null && (o = {});
+    this.model || (this.model = new Graph);
+    this.id = this.graph_id = _.domize('graph', this.model.id || this.model.get('slug') || this.model.cid);
+    GraphView.__super__.initialize.apply(this, arguments);
+    for (_i = 0, _len = (_ref = this.__debounce__).length; _i < _len; ++_i) {
+      name = _ref[_i];
+      this[name] = _.debounce(this[name], DEBOUNCE_RENDER);
+    }
+    this.on('start-waiting', this.onStartWaiting, this);
+    this.on('stop-waiting', this.onStopWaiting, this);
+    if (this.waitingOn) {
+      this.onStartWaiting();
+    }
+    this.on('update', this.onUpdate, this);
+    this.model.on('start-waiting', this.wait, this).on('stop-waiting', this.unwait, this).on('sync', this.onSync, this).on('destroy', this.remove, this).on('change', this.render, this).on('change:dataset', this.onModelChange, this).on('change:options', this.onModelChange, this).on('error', this.onModelError, this);
+    this.resizeViewport();
+    return $(root).on('resize', _.debounce(this.resizeViewport, DEBOUNCE_RENDER));
+  },
+  loadData: function(){
+    var _this = this;
+    this.resizeViewport();
+    this.wait();
+    return Seq().seq_(function(next){
+      return _this.model.once('ready', next.ok).load();
+    }).seq_(function(next){
+      return _this.model.chartType.once('ready', next.ok);
+    }).seq_(function(next){
+      return _this.model.once('data-ready', next.ok).loadData();
+    }).seq(function(){
+      _this.unwait();
+      return _this.onReady();
+    });
+  },
+  onReady: function(){
+    if (this.ready) {
+      return;
+    }
+    this.triggerReady();
+    return this.onSync();
+  }
+  /**
+   * Reload the graph definition from the server.
+   */,
+  load: function(){
+    console.log(this + ".load!");
+    this.wait();
+    this.model.fetch({
+      success: this.unwait,
+      error: this.unwait
+    });
+    return false;
+  }
+  /**
+   * Save the graph definition to the server.
+   */,
+  save: function(){
+    var id;
+    console.log(this + ".save!");
+    this.wait();
+    id = this.model.get('slug') || this.model.id;
+    this.model.save({
+      id: id
+    }, {
+      wait: true,
+      success: this.unwait,
+      error: this.unwait
+    });
+    return false;
+  }
+  /**
+   * Flush all changes.
+   */,
+  change: function(){
+    this.model.change();
+    return this;
+  },
+  chartOptions: function(values, opts){
+    var k, v, 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);
+    }
+    values || (values = {});
+    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;
+  },
+  toTemplateLocals: function(){
+    var attrs, that, callout, yoy, mom;
+    attrs = _.extend({}, this.model.attributes);
+    if (that = attrs.desc) {
+      attrs.desc = jade.filters.markdown(that);
+    }
+    if (that = attrs.notes) {
+      attrs.notes = jade.filters.markdown(that);
+    }
+    delete attrs.options;
+    delete attrs.callout;
+    if (callout = this.model.getCalloutData()) {
+      yoy = callout.year, mom = callout.month;
+      attrs.callout = {
+        latest: this.model.chartType.numberFormatter(callout.latest, 2, false).toString(),
+        year: {
+          dates: yoy.dates.map(function(it){
+            return moment(it).format('MMM YY');
+          }).join(' &mdash; '),
+          value: (100 * yoy.value[2] / yoy.value[0]).toFixed(2) + '%',
+          delta: yoy.value[2]
+        },
+        month: {
+          dates: mom.dates.map(function(it){
+            return moment(it).format('MMM YY');
+          }).join(' &mdash; '),
+          value: (100 * mom.value[2] / mom.value[0]).toFixed(2) + '%',
+          delta: mom.value[2]
+        }
+      };
+    }
+    return __import({
+      model: this.model,
+      graph_id: this.graph_id,
+      view: this,
+      slug: '',
+      name: '',
+      desc: '',
+      callout: {
+        latest: '',
+        year: {
+          dates: '',
+          value: ''
+        },
+        month: {
+          dates: '',
+          value: ''
+        }
+      }
+    }, attrs);
+  }
+  /**
+   * Resize the viewport to the model-specified bounds.
+   */,
+  resizeViewport: function(){
+    var _ref;
+    return (_ref = this.model.chartType) != null ? _ref.withView(this).resizeViewport() : void 8;
+  }
+  /**
+   * Redraw chart inside viewport.
+   */,
+  renderChart: function(){
+    var _ref;
+    this.chart = (_ref = this.model.chartType) != null ? _ref.withView(this).render() : void 8;
+    return this;
+  }
+  /**
+   * Render the chart and other Graph-derived view components.
+   */,
+  render: function(){
+    if (!this.ready) {
+      return this;
+    }
+    this.wait();
+    this.checkWaiting();
+    GraphView.__super__.render.apply(this, arguments);
+    this.renderChart();
+    this.unwait();
+    this.checkWaiting();
+    return this;
+  },
+  onUpdate: function(self, locals){
+    var co, el;
+    co = locals.callout;
+    el = this.$('.callout');
+    el.find('.metric-change .value').removeClass('delta-positive delta-negative');
+    if (co.year.delta > 0) {
+      el.find(' .metric-change.year-over-year .value').addClass('delta-positive');
+    } else if (co.year.delta < 0) {
+      el.find(' .metric-change.year-over-year .value').addClass('delta-negative');
+    }
+    if (co.month.delta > 0) {
+      el.find(' .metric-change.month-over-month .value').addClass('delta-positive');
+    } else if (co.month.delta < 0) {
+      el.find(' .metric-change.month-over-month .value').addClass('delta-negative');
+    }
+    return this;
+  },
+  onSync: function(){
+    if (!this.ready) {
+      return;
+    }
+    console.info(this + ".sync() --> success!");
+    this.chartOptions(this.model.getOptions(), {
+      silent: true
+    });
+    return this.render();
+  },
+  onStartWaiting: function(){
+    var status;
+    return status = this.checkWaiting();
+  },
+  onStopWaiting: function(){
+    var status;
+    return status = this.checkWaiting();
+  },
+  onModelError: function(){
+    return console.error(this + ".error!", arguments);
+  },
+  onModelChange: function(){
+    var changes, options;
+    changes = this.model.changedAttributes();
+    options = this.model.getOptions();
+    if (changes != null && changes.options) {
+      return this.chartOptions(options, {
+        silent: true
+      });
+    }
+  },
+  stopAndRender: function(){
+    this.render.apply(this, arguments);
+    return false;
+  }
+  /**
+   * Retrieve or construct the spinner.
+   */,
+  spinner: function(){
+    var el, opts, isHidden;
+    el = this.$('.graph-spinner');
+    if (!el.data('spinner')) {
+      opts = {
+        lines: 9,
+        length: 2,
+        width: 1,
+        radius: 7,
+        rotate: -10.5,
+        trail: 50,
+        opacity: 1 / 4,
+        shadow: false,
+        speed: 1,
+        zIndex: 2e9,
+        color: '#000',
+        top: 'auto',
+        left: 'auto',
+        className: 'spinner',
+        fps: 20,
+        hwaccel: Modernizr.csstransforms3d
+      };
+      isHidden = el.css('display') === 'none';
+      el.show().spin(opts);
+      if (isHidden) {
+        el.hide();
+      }
+    }
+    return el;
+  },
+  checkWaiting: function(){
+    var spinner, isWaiting;
+    spinner = this.spinner();
+    if (isWaiting = this.waitingOn > 0) {
+      spinner.show();
+      if (spinner.find('.spinner').css('top') === '0px') {
+        spinner.spin(false);
+        this.spinner();
+      }
+    } else {
+      spinner.hide();
+    }
+    return isWaiting;
+  }
+});
+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/graph/graph-view.mod.js b/lib/graph/graph-view.mod.js
new file mode 100644 (file)
index 0000000..c3bf4cb
--- /dev/null
@@ -0,0 +1,315 @@
+require.define('/node_modules/kraken/graph/graph-view.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var moment, BaseView, Graph, root, DEBOUNCE_RENDER, GraphView, _;
+moment = require('moment');
+_ = require('kraken/util/underscore');
+BaseView = require('kraken/base').BaseView;
+Graph = require('kraken/graph/graph-model').Graph;
+root = function(){
+  return this;
+}();
+DEBOUNCE_RENDER = 100;
+/**
+ * @class Base view for a Graph visualizations.
+ */
+GraphView = exports.GraphView = BaseView.extend({
+  FILTER_CHART_OPTIONS: ['file', 'labels', 'visibility', 'colors', 'dateWindow', 'ticker', 'timingName', 'xValueParser', 'axisLabelFormatter', 'xAxisLabelFormatter', 'yAxisLabelFormatter', 'valueFormatter', 'xValueFormatter', 'yValueFormatter'],
+  __bind__: ['render', 'stopAndRender', 'resizeViewport', 'checkWaiting', 'onReady', 'onSync', 'onModelChange'],
+  __debounce__: ['render'],
+  tagName: 'section'
+  /**
+   * The chart type backing this graph.
+   * @type ChartType
+   */,
+  chartType: null,
+  constructor: (function(){
+    function GraphView(){
+      return BaseView.apply(this, arguments);
+    }
+    return GraphView;
+  }()),
+  initialize: function(o){
+    var name, _i, _ref, _len;
+    o == null && (o = {});
+    this.model || (this.model = new Graph);
+    this.id = this.graph_id = _.domize('graph', this.model.id || this.model.get('slug') || this.model.cid);
+    GraphView.__super__.initialize.apply(this, arguments);
+    for (_i = 0, _len = (_ref = this.__debounce__).length; _i < _len; ++_i) {
+      name = _ref[_i];
+      this[name] = _.debounce(this[name], DEBOUNCE_RENDER);
+    }
+    this.on('start-waiting', this.onStartWaiting, this);
+    this.on('stop-waiting', this.onStopWaiting, this);
+    if (this.waitingOn) {
+      this.onStartWaiting();
+    }
+    this.on('update', this.onUpdate, this);
+    this.model.on('start-waiting', this.wait, this).on('stop-waiting', this.unwait, this).on('sync', this.onSync, this).on('destroy', this.remove, this).on('change', this.render, this).on('change:dataset', this.onModelChange, this).on('change:options', this.onModelChange, this).on('error', this.onModelError, this);
+    this.resizeViewport();
+    return $(root).on('resize', _.debounce(this.resizeViewport, DEBOUNCE_RENDER));
+  },
+  loadData: function(){
+    var _this = this;
+    this.resizeViewport();
+    this.wait();
+    return Seq().seq_(function(next){
+      return _this.model.once('ready', next.ok).load();
+    }).seq_(function(next){
+      return _this.model.chartType.once('ready', next.ok);
+    }).seq_(function(next){
+      return _this.model.once('data-ready', next.ok).loadData();
+    }).seq(function(){
+      _this.unwait();
+      return _this.onReady();
+    });
+  },
+  onReady: function(){
+    if (this.ready) {
+      return;
+    }
+    this.triggerReady();
+    return this.onSync();
+  }
+  /**
+   * Reload the graph definition from the server.
+   */,
+  load: function(){
+    console.log(this + ".load!");
+    this.wait();
+    this.model.fetch({
+      success: this.unwait,
+      error: this.unwait
+    });
+    return false;
+  }
+  /**
+   * Save the graph definition to the server.
+   */,
+  save: function(){
+    var id;
+    console.log(this + ".save!");
+    this.wait();
+    id = this.model.get('slug') || this.model.id;
+    this.model.save({
+      id: id
+    }, {
+      wait: true,
+      success: this.unwait,
+      error: this.unwait
+    });
+    return false;
+  }
+  /**
+   * Flush all changes.
+   */,
+  change: function(){
+    this.model.change();
+    return this;
+  },
+  chartOptions: function(values, opts){
+    var k, v, 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);
+    }
+    values || (values = {});
+    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;
+  },
+  toTemplateLocals: function(){
+    var attrs, that, callout, yoy, mom;
+    attrs = _.extend({}, this.model.attributes);
+    if (that = attrs.desc) {
+      attrs.desc = jade.filters.markdown(that);
+    }
+    if (that = attrs.notes) {
+      attrs.notes = jade.filters.markdown(that);
+    }
+    delete attrs.options;
+    delete attrs.callout;
+    if (callout = this.model.getCalloutData()) {
+      yoy = callout.year, mom = callout.month;
+      attrs.callout = {
+        latest: this.model.chartType.numberFormatter(callout.latest, 2, false).toString(),
+        year: {
+          dates: yoy.dates.map(function(it){
+            return moment(it).format('MMM YY');
+          }).join(' &mdash; '),
+          value: (100 * yoy.value[2] / yoy.value[0]).toFixed(2) + '%',
+          delta: yoy.value[2]
+        },
+        month: {
+          dates: mom.dates.map(function(it){
+            return moment(it).format('MMM YY');
+          }).join(' &mdash; '),
+          value: (100 * mom.value[2] / mom.value[0]).toFixed(2) + '%',
+          delta: mom.value[2]
+        }
+      };
+    }
+    return __import({
+      model: this.model,
+      graph_id: this.graph_id,
+      view: this,
+      slug: '',
+      name: '',
+      desc: '',
+      callout: {
+        latest: '',
+        year: {
+          dates: '',
+          value: ''
+        },
+        month: {
+          dates: '',
+          value: ''
+        }
+      }
+    }, attrs);
+  }
+  /**
+   * Resize the viewport to the model-specified bounds.
+   */,
+  resizeViewport: function(){
+    var _ref;
+    return (_ref = this.model.chartType) != null ? _ref.withView(this).resizeViewport() : void 8;
+  }
+  /**
+   * Redraw chart inside viewport.
+   */,
+  renderChart: function(){
+    var _ref;
+    this.chart = (_ref = this.model.chartType) != null ? _ref.withView(this).render() : void 8;
+    return this;
+  }
+  /**
+   * Render the chart and other Graph-derived view components.
+   */,
+  render: function(){
+    if (!this.ready) {
+      return this;
+    }
+    this.wait();
+    this.checkWaiting();
+    GraphView.__super__.render.apply(this, arguments);
+    this.renderChart();
+    this.unwait();
+    this.checkWaiting();
+    return this;
+  },
+  onUpdate: function(self, locals){
+    var co, el;
+    co = locals.callout;
+    el = this.$('.callout');
+    el.find('.metric-change .value').removeClass('delta-positive delta-negative');
+    if (co.year.delta > 0) {
+      el.find(' .metric-change.year-over-year .value').addClass('delta-positive');
+    } else if (co.year.delta < 0) {
+      el.find(' .metric-change.year-over-year .value').addClass('delta-negative');
+    }
+    if (co.month.delta > 0) {
+      el.find(' .metric-change.month-over-month .value').addClass('delta-positive');
+    } else if (co.month.delta < 0) {
+      el.find(' .metric-change.month-over-month .value').addClass('delta-negative');
+    }
+    return this;
+  },
+  onSync: function(){
+    if (!this.ready) {
+      return;
+    }
+    console.info(this + ".sync() --> success!");
+    this.chartOptions(this.model.getOptions(), {
+      silent: true
+    });
+    return this.render();
+  },
+  onStartWaiting: function(){
+    var status;
+    return status = this.checkWaiting();
+  },
+  onStopWaiting: function(){
+    var status;
+    return status = this.checkWaiting();
+  },
+  onModelError: function(){
+    return console.error(this + ".error!", arguments);
+  },
+  onModelChange: function(){
+    var changes, options;
+    changes = this.model.changedAttributes();
+    options = this.model.getOptions();
+    if (changes != null && changes.options) {
+      return this.chartOptions(options, {
+        silent: true
+      });
+    }
+  },
+  stopAndRender: function(){
+    this.render.apply(this, arguments);
+    return false;
+  }
+  /**
+   * Retrieve or construct the spinner.
+   */,
+  spinner: function(){
+    var el, opts, isHidden;
+    el = this.$('.graph-spinner');
+    if (!el.data('spinner')) {
+      opts = {
+        lines: 9,
+        length: 2,
+        width: 1,
+        radius: 7,
+        rotate: -10.5,
+        trail: 50,
+        opacity: 1 / 4,
+        shadow: false,
+        speed: 1,
+        zIndex: 2e9,
+        color: '#000',
+        top: 'auto',
+        left: 'auto',
+        className: 'spinner',
+        fps: 20,
+        hwaccel: Modernizr.csstransforms3d
+      };
+      isHidden = el.css('display') === 'none';
+      el.show().spin(opts);
+      if (isHidden) {
+        el.hide();
+      }
+    }
+    return el;
+  },
+  checkWaiting: function(){
+    var spinner, isWaiting;
+    spinner = this.spinner();
+    if (isWaiting = this.waitingOn > 0) {
+      spinner.show();
+      if (spinner.find('.spinner').css('top') === '0px') {
+        spinner.spin(false);
+        this.spinner();
+      }
+    } else {
+      spinner.hide();
+    }
+    return isWaiting;
+  }
+});
+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/graph/index.js b/lib/graph/index.js
new file mode 100644 (file)
index 0000000..8817c26
--- /dev/null
@@ -0,0 +1,12 @@
+var models, base_views, display_views, edit_views, index_views;
+models = require('kraken/graph/graph-model');
+base_views = require('kraken/graph/graph-view');
+display_views = require('kraken/graph/graph-display-view');
+edit_views = require('kraken/graph/graph-edit-view');
+index_views = require('kraken/graph/graph-list-view');
+__import(__import(__import(__import(__import(exports, models), base_views), display_views), edit_views), index_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/graph/index.mod.js b/lib/graph/index.mod.js
new file mode 100644 (file)
index 0000000..f57db69
--- /dev/null
@@ -0,0 +1,16 @@
+require.define('/node_modules/kraken/graph/index.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var models, base_views, display_views, edit_views, index_views;
+models = require('kraken/graph/graph-model');
+base_views = require('kraken/graph/graph-view');
+display_views = require('kraken/graph/graph-display-view');
+edit_views = require('kraken/graph/graph-edit-view');
+index_views = require('kraken/graph/graph-list-view');
+__import(__import(__import(__import(__import(exports, models), base_views), display_views), edit_views), index_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/main-dashboard.js b/lib/main-dashboard.js
new file mode 100644 (file)
index 0000000..a9fabe2
--- /dev/null
@@ -0,0 +1,30 @@
+var Seq, Backbone, op, AppView, BaseView, BaseModel, BaseList, ChartType, DygraphsChartType, Graph, GraphList, GraphDisplayView, DashboardView, Dashboard, root, main, _ref, _;
+Seq = require('seq');
+Backbone = require('backbone');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+AppView = require('kraken/app').AppView;
+_ref = require('kraken/base'), BaseView = _ref.BaseView, BaseModel = _ref.BaseModel, BaseList = _ref.BaseList;
+_ref = require('kraken/chart'), ChartType = _ref.ChartType, DygraphsChartType = _ref.DygraphsChartType;
+_ref = require('kraken/graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView;
+_ref = require('kraken/dashboard'), DashboardView = _ref.DashboardView, Dashboard = _ref.Dashboard;
+root = this;
+main = function(){
+  var loc, data, match, id;
+  loc = String(root.location);
+  data = {};
+  if (match = /\/dashboards\/([^\/?]+)/i.exec(loc)) {
+    id = match[1];
+    if (!_(['edit', 'new']).contains(id)) {
+      data.id = data.slug = id;
+    }
+  }
+  return root.app = new AppView(function(){
+    this.model = root.dashboard = new Dashboard(data, {
+      parse: true
+    });
+    return this.view = root.view = new DashboardView({
+      model: this.model
+    });
+  });
+};
+jQuery(main);
\ No newline at end of file
diff --git a/lib/main-display.js b/lib/main-display.js
new file mode 100644 (file)
index 0000000..a497afc
--- /dev/null
@@ -0,0 +1,32 @@
+var Seq, Backbone, op, AppView, BaseView, BaseModel, BaseList, ChartType, DygraphsChartType, Graph, GraphList, GraphDisplayView, root, CHART_OPTIONS_SPEC, CHART_DEFAULT_OPTIONS, main, _ref, _;
+Seq = require('seq');
+Backbone = require('backbone');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+AppView = require('kraken/app').AppView;
+_ref = require('kraken/base'), BaseView = _ref.BaseView, BaseModel = _ref.BaseModel, BaseList = _ref.BaseList;
+_ref = require('kraken/chart'), ChartType = _ref.ChartType, DygraphsChartType = _ref.DygraphsChartType;
+_ref = require('kraken/graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView;
+root = this;
+CHART_OPTIONS_SPEC = [];
+CHART_DEFAULT_OPTIONS = {};
+main = function(){
+  var loc, data, match, id;
+  History.Adapter.bind(window, 'statechange', function(){});
+  loc = String(root.location);
+  data = {};
+  if (match = /\/graphs\/([^\/?]+)/i.exec(loc)) {
+    id = match[1];
+    if (!_(['edit', 'new']).contains(id)) {
+      data.id = data.slug = id;
+    }
+  }
+  return root.app = new AppView(function(){
+    this.model = root.graph = new Graph(data, {
+      parse: true
+    });
+    return this.view = root.view = new GraphDisplayView({
+      model: this.model
+    });
+  });
+};
+jQuery(main);
\ No newline at end of file
diff --git a/lib/main-edit.js b/lib/main-edit.js
new file mode 100644 (file)
index 0000000..3d156ab
--- /dev/null
@@ -0,0 +1,33 @@
+var Seq, Backbone, op, AppView, BaseView, BaseModel, BaseList, ChartType, DataSource, DataSourceList, Graph, GraphList, GraphEditView, root, CHART_OPTIONS_SPEC, CHART_DEFAULT_OPTIONS, main, _ref, _;
+Seq = require('seq');
+Backbone = require('backbone');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+AppView = require('kraken/app').AppView;
+_ref = require('kraken/base'), BaseView = _ref.BaseView, BaseModel = _ref.BaseModel, BaseList = _ref.BaseList;
+ChartType = require('kraken/chart').ChartType;
+_ref = require('kraken/data'), DataSource = _ref.DataSource, DataSourceList = _ref.DataSourceList;
+_ref = require('kraken/graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphEditView = _ref.GraphEditView;
+root = this;
+CHART_OPTIONS_SPEC = [];
+CHART_DEFAULT_OPTIONS = {};
+main = function(){
+  var loc, data, match, id;
+  History.Adapter.bind(window, 'statechange', function(){});
+  loc = String(root.location);
+  data = {};
+  if (match = /\/graphs\/([^\/?]+)/i.exec(loc)) {
+    id = match[1];
+    if (!_(['edit', 'new']).contains(id)) {
+      data.id = data.slug = id;
+    }
+  }
+  return root.app = new AppView(function(){
+    this.model = root.graph = new Graph(data, {
+      parse: true
+    });
+    return this.view = root.view = new GraphEditView({
+      model: this.model
+    });
+  });
+};
+jQuery(main);
\ No newline at end of file
diff --git a/lib/main-geo.js b/lib/main-geo.js
new file mode 100644 (file)
index 0000000..f406cd6
--- /dev/null
@@ -0,0 +1,102 @@
+var Seq, d3, ColorBrewer, infobox, feature, map, data, width, height, fill, quantize, move, projection, path, zoom, spinner, main;
+Seq = require('seq');
+d3 = require('d3');
+ColorBrewer = require('colorbrewer');
+data = map = feature = infobox = null;
+width = 960;
+height = 500;
+fill = d3.scale.log().domain([1, 10000]).range(["black", "red"]);
+quantize = function(d){
+  if (data[d.properties.name] != null) {
+    return fill(data[d.properties.name]['editors']);
+  } else {
+    return fill("rgb(0,0,0)");
+  }
+};
+move = function(){
+  projection.translate(d3.event.translate).scale(d3.event.scale);
+  return feature.attr("d", path);
+};
+projection = d3.geo.mercator().scale(width).translate([width / 2, height / 2]);
+path = d3.geo.path().projection(projection);
+zoom = d3.behavior.zoom().translate(projection.translate()).scale(projection.scale()).scaleExtent([height, height * 8]).on("zoom", move);
+spinner = function(overrides){
+  var opts;
+  overrides == null && (overrides = {});
+  opts = __import({
+    lines: 11,
+    length: 4,
+    width: 1,
+    radius: 18,
+    rotate: -10.5,
+    trail: 50,
+    opacity: 1 / 4,
+    shadow: false,
+    speed: 1,
+    zIndex: 2e9,
+    color: '#333',
+    top: 'auto',
+    left: 'auto',
+    className: 'spinner',
+    fps: 20,
+    hwaccel: Modernizr.csstransforms3d
+  }, overrides);
+  return jQuery('.geo-spinner').show().spin(opts);
+};
+spinner();
+main = function(){
+  var setInfoBox, worldmap;
+  map = d3.select('#worldmap').append("svg:svg").attr("width", width).attr("height", height).append("svg:g").attr("transform", "translate(0,0)").call(zoom);
+  feature = map.selectAll(".feature");
+  map.append("svg:rect").attr("class", "frame").attr("width", width).attr("height", height);
+  infobox = d3.select('#infobox');
+  infobox.select('#ball').append("svg:svg").attr("width", "100%").attr("height", "20px").append("svg:rect").attr("width", "60%").attr("height", "20px").attr("fill", '#f40500');
+  setInfoBox = function(d){
+    var name, ae, e5, e100, xy;
+    name = d.properties.name;
+    ae = 0;
+    e5 = 0;
+    e100 = 0;
+    if (data[name] != null) {
+      ae = parseInt(data[name].editors);
+      e5 = parseInt(data[name].editors5);
+      e100 = parseInt(data[name].editors100);
+    }
+    infobox.select('#country').text(name);
+    infobox.select('#ae').text(ae);
+    infobox.select('#e5').text(e5 + " (" + (100.0 * e5 / ae).toPrecision(3) + "%)");
+    infobox.select('#e100').text(e100 + " (" + (100.0 * e100 / ae).toPrecision(3) + "%)");
+    xy = d3.svg.mouse(this);
+    infobox.style("left", xy[0] + 'px');
+    infobox.style("top", xy[1] + 'px');
+    return infobox.style("display", "block");
+  };
+  worldmap = function(){
+    return d3.json("/data/geo/maps/world-countries.json", function(json){
+      return feature = feature.data(json.features).enter().append("svg:path").attr("class", "feature").attr("d", path).attr("fill", quantize).attr("id", function(d){
+        return d.properties.name;
+      }).on("mouseover", setInfoBox).on("mouseout", function(){
+        return infobox.style("display", "none");
+      });
+    });
+  };
+  return jQuery.ajax({
+    url: "/data/geo/data/en_geo_editors.json",
+    dataType: 'json',
+    success: function(res){
+      data = res;
+      jQuery('.geo-spinner').spin(false).hide();
+      worldmap();
+      return console.log('Loaded geo coding map!');
+    },
+    error: function(err){
+      return console.error(err);
+    }
+  });
+};
+jQuery(main);
+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/main-graph-list.js b/lib/main-graph-list.js
new file mode 100644 (file)
index 0000000..79c0053
--- /dev/null
@@ -0,0 +1,23 @@
+var op, BaseView, BaseModel, BaseList, Graph, GraphList, GraphListView, main, graph_list_url, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_ref = require('kraken/base'), BaseView = _ref.BaseView, BaseModel = _ref.BaseModel, BaseList = _ref.BaseList;
+_ref = require('kraken/graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphListView = _ref.GraphListView;
+main = function(graph_list_data){
+  var graphs, view;
+  graphs = new GraphList(graph_list_data);
+  view = new GraphListView({
+    'collection': graphs
+  });
+  return $('#content .inner').append(view.el);
+};
+graph_list_url = '/graphs.json';
+jQuery.ajax({
+  url: graph_list_url,
+  dataType: 'json',
+  success: function(res){
+    return jQuery(main.bind(this, res));
+  },
+  error: function(err){
+    return console.error(err);
+  }
+});
\ No newline at end of file
diff --git a/lib/template/browser-helpers.jade.js b/lib/template/browser-helpers.jade.js
new file mode 100644 (file)
index 0000000..a89b630
--- /dev/null
@@ -0,0 +1,13 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ window.Markdown || (window.Markdown = new (require('showdown').Showdown).converter());
+ (jade.filters || (jade.filters = {})).markdown = function (s, name){ return s && Markdown.makeHtml(s.replace(/\n/g, '\n\n')); };
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/chart/chart-option.jade.js b/lib/template/chart/chart-option.jade.js
new file mode 100644 (file)
index 0000000..60be58f
--- /dev/null
@@ -0,0 +1,107 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ window.Markdown || (window.Markdown = new (require('showdown').Showdown).converter());
+ (jade.filters || (jade.filters = {})).markdown = function (s, name){ return s && Markdown.makeHtml(s.replace(/\n/g, '\n\n')); };
+ var option_id    = _.domize('option', id)
+ var value_id     = _.domize('value', id)
+ var type_cls     = _.domize('type', type)
+ var category_cls = _.domize('category', model.getCategoryIndex()) + ' ' + _.domize('category', model.getCategory())
+ var tags_cls     = tags.map(_.domize('tag')).join(' ')
+buf.push('\n<section');
+buf.push(attrs({ 'id':(option_id), "class": ('chart-option') + ' ' + ('field') + ' ' + ('isotope-item') + ' ' + ("" + (category_cls) + " " + (tags_cls) + "") }));
+buf.push('><a');
+buf.push(attrs({ 'title':("Click to collapse"), "class": ('close') }));
+buf.push('>&times;</a>\n  <h3');
+buf.push(attrs({ 'title':("Click to collapse"), "class": ('shortname') }));
+buf.push('>' + escape((interp = name) == null ? '' : interp) + '\n  </h3>\n  <label');
+buf.push(attrs({ 'for':(value_id), "class": ('name') }));
+buf.push('>' + escape((interp = name) == null ? '' : interp) + '\n  </label>');
+if ( ( /object|array|function/i.test(type) ))
+{
+buf.push('\n  <textarea');
+buf.push(attrs({ 'id':(value_id), 'name':(name), 'data-bind':("value"), "class": ('value') + ' ' + (type_cls) }));
+buf.push('>' + escape((interp = value) == null ? '' : interp) + '\n  </textarea>');
+}
+else
+{
+ var input_type = (/boolean/i.test(type) ? 'checkbox' : 'text');
+ var checked = ((/boolean/i.test(type) && value) ? 'checked' : null);
+buf.push('\n  <input');
+buf.push(attrs({ 'type':(input_type), 'id':(value_id), 'name':(name), 'value':(value), 'checked':(checked), 'data-bind':("value"), "class": ('value') + ' ' + (type_cls) }));
+buf.push('/>');
+}
+buf.push('\n  <div');
+buf.push(attrs({ "class": ('type') + ' ' + (type_cls) }));
+buf.push('>' + escape((interp = type) == null ? '' : interp) + '\n  </div>\n  <div');
+buf.push(attrs({ 'title':("Default: " + (def) + " (" + (type) + ")"), "class": ('default') + ' ' + (type_cls) }));
+buf.push('>' + escape((interp = def) == null ? '' : interp) + '\n  </div>\n  <div');
+buf.push(attrs({ "class": ('desc') }));
+buf.push('>');
+var __val__ = jade.filters.markdown(desc)
+buf.push(null == __val__ ? "" : __val__);
+buf.push('\n  </div>\n  <div');
+buf.push(attrs({ "class": ('tags') }));
+buf.push('>');
+// iterate tags
+(function(){
+  if ('number' == typeof tags.length) {
+    for (var $index = 0, $$l = tags.length; $index < $$l; $index++) {
+      var tag = tags[$index];
+
+ var tag_cls = _.domize('tag',tag) + ' ' + _.domize('category',model.getTagIndex(tag))
+buf.push('<span');
+buf.push(attrs({ "class": ('tag') + ' ' + (tag_cls) }));
+buf.push('>' + escape((interp = tag) == null ? '' : interp) + '</span> \n');
+    }
+  } else {
+    for (var $index in tags) {
+      var tag = tags[$index];
+
+ var tag_cls = _.domize('tag',tag) + ' ' + _.domize('category',model.getTagIndex(tag))
+buf.push('<span');
+buf.push(attrs({ "class": ('tag') + ' ' + (tag_cls) }));
+buf.push('>' + escape((interp = tag) == null ? '' : interp) + '</span> \n');
+   }
+  }
+}).call(this);
+
+buf.push('\n  </div>\n  <div');
+buf.push(attrs({ 'data-toggle':("collapse"), 'data-target':("#" + (option_id) + " .examples ul"), "class": ('examples') }));
+buf.push('>\n    <ul');
+buf.push(attrs({ "class": ('collapse') }));
+buf.push('>');
+// iterate examples
+(function(){
+  if ('number' == typeof examples.length) {
+    for (var $index = 0, $$l = examples.length; $index < $$l; $index++) {
+      var example = examples[$index];
+
+buf.push('\n      <li');
+buf.push(attrs({ "class": ('example') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("http://dygraphs.com/tests/" + (example) + ".html"), 'target':("_blank") }));
+buf.push('>' + escape((interp = example) == null ? '' : interp) + '</a>\n      </li>');
+    }
+  } else {
+    for (var $index in examples) {
+      var example = examples[$index];
+
+buf.push('\n      <li');
+buf.push(attrs({ "class": ('example') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("http://dygraphs.com/tests/" + (example) + ".html"), 'target':("_blank") }));
+buf.push('>' + escape((interp = example) == null ? '' : interp) + '</a>\n      </li>');
+   }
+  }
+}).call(this);
+
+buf.push('\n    </ul>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/chart/chart-option.jade.mod.js b/lib/template/chart/chart-option.jade.mod.js
new file mode 100644 (file)
index 0000000..7d3f16d
--- /dev/null
@@ -0,0 +1,111 @@
+require.define('/node_modules/kraken/template/chart/chart-option.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ window.Markdown || (window.Markdown = new (require('showdown').Showdown).converter());
+ (jade.filters || (jade.filters = {})).markdown = function (s, name){ return s && Markdown.makeHtml(s.replace(/\n/g, '\n\n')); };
+ var option_id    = _.domize('option', id)
+ var value_id     = _.domize('value', id)
+ var type_cls     = _.domize('type', type)
+ var category_cls = _.domize('category', model.getCategoryIndex()) + ' ' + _.domize('category', model.getCategory())
+ var tags_cls     = tags.map(_.domize('tag')).join(' ')
+buf.push('\n<section');
+buf.push(attrs({ 'id':(option_id), "class": ('chart-option') + ' ' + ('field') + ' ' + ('isotope-item') + ' ' + ("" + (category_cls) + " " + (tags_cls) + "") }));
+buf.push('><a');
+buf.push(attrs({ 'title':("Click to collapse"), "class": ('close') }));
+buf.push('>&times;</a>\n  <h3');
+buf.push(attrs({ 'title':("Click to collapse"), "class": ('shortname') }));
+buf.push('>' + escape((interp = name) == null ? '' : interp) + '\n  </h3>\n  <label');
+buf.push(attrs({ 'for':(value_id), "class": ('name') }));
+buf.push('>' + escape((interp = name) == null ? '' : interp) + '\n  </label>');
+if ( ( /object|array|function/i.test(type) ))
+{
+buf.push('\n  <textarea');
+buf.push(attrs({ 'id':(value_id), 'name':(name), 'data-bind':("value"), "class": ('value') + ' ' + (type_cls) }));
+buf.push('>' + escape((interp = value) == null ? '' : interp) + '\n  </textarea>');
+}
+else
+{
+ var input_type = (/boolean/i.test(type) ? 'checkbox' : 'text');
+ var checked = ((/boolean/i.test(type) && value) ? 'checked' : null);
+buf.push('\n  <input');
+buf.push(attrs({ 'type':(input_type), 'id':(value_id), 'name':(name), 'value':(value), 'checked':(checked), 'data-bind':("value"), "class": ('value') + ' ' + (type_cls) }));
+buf.push('/>');
+}
+buf.push('\n  <div');
+buf.push(attrs({ "class": ('type') + ' ' + (type_cls) }));
+buf.push('>' + escape((interp = type) == null ? '' : interp) + '\n  </div>\n  <div');
+buf.push(attrs({ 'title':("Default: " + (def) + " (" + (type) + ")"), "class": ('default') + ' ' + (type_cls) }));
+buf.push('>' + escape((interp = def) == null ? '' : interp) + '\n  </div>\n  <div');
+buf.push(attrs({ "class": ('desc') }));
+buf.push('>');
+var __val__ = jade.filters.markdown(desc)
+buf.push(null == __val__ ? "" : __val__);
+buf.push('\n  </div>\n  <div');
+buf.push(attrs({ "class": ('tags') }));
+buf.push('>');
+// iterate tags
+(function(){
+  if ('number' == typeof tags.length) {
+    for (var $index = 0, $$l = tags.length; $index < $$l; $index++) {
+      var tag = tags[$index];
+
+ var tag_cls = _.domize('tag',tag) + ' ' + _.domize('category',model.getTagIndex(tag))
+buf.push('<span');
+buf.push(attrs({ "class": ('tag') + ' ' + (tag_cls) }));
+buf.push('>' + escape((interp = tag) == null ? '' : interp) + '</span> \n');
+    }
+  } else {
+    for (var $index in tags) {
+      var tag = tags[$index];
+
+ var tag_cls = _.domize('tag',tag) + ' ' + _.domize('category',model.getTagIndex(tag))
+buf.push('<span');
+buf.push(attrs({ "class": ('tag') + ' ' + (tag_cls) }));
+buf.push('>' + escape((interp = tag) == null ? '' : interp) + '</span> \n');
+   }
+  }
+}).call(this);
+
+buf.push('\n  </div>\n  <div');
+buf.push(attrs({ 'data-toggle':("collapse"), 'data-target':("#" + (option_id) + " .examples ul"), "class": ('examples') }));
+buf.push('>\n    <ul');
+buf.push(attrs({ "class": ('collapse') }));
+buf.push('>');
+// iterate examples
+(function(){
+  if ('number' == typeof examples.length) {
+    for (var $index = 0, $$l = examples.length; $index < $$l; $index++) {
+      var example = examples[$index];
+
+buf.push('\n      <li');
+buf.push(attrs({ "class": ('example') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("http://dygraphs.com/tests/" + (example) + ".html"), 'target':("_blank") }));
+buf.push('>' + escape((interp = example) == null ? '' : interp) + '</a>\n      </li>');
+    }
+  } else {
+    for (var $index in examples) {
+      var example = examples[$index];
+
+buf.push('\n      <li');
+buf.push(attrs({ "class": ('example') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("http://dygraphs.com/tests/" + (example) + ".html"), 'target':("_blank") }));
+buf.push('>' + escape((interp = example) == null ? '' : interp) + '</a>\n      </li>');
+   }
+  }
+}).call(this);
+
+buf.push('\n    </ul>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/chart/chart-scaffold.jade.js b/lib/template/chart/chart-scaffold.jade.js
new file mode 100644 (file)
index 0000000..49dc47e
--- /dev/null
@@ -0,0 +1,28 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<form');
+buf.push(attrs({ "class": ('chart-options') + ' ' + ('scaffold') + ' ' + ('form-inline') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('chart-options-controls') + ' ' + ('control-group') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('collapse-all-options-button') + ' ' + ('btn') }));
+buf.push('>Collapse All</a><a');
+buf.push(attrs({ 'href':("#"), "class": ('expand-all-options-button') + ' ' + ('btn') }));
+buf.push('>Expand All</a>\n    <div');
+buf.push(attrs({ 'data-toggle':("buttons-radio"), "class": ('std-adv-filter-buttons') + ' ' + ('btn-group') + ' ' + ('pull-right') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), 'data-filter':(".tag_standard"), "class": ('standard-filter-button') + ' ' + ('options-filter-button') + ' ' + ('btn') + ' ' + ('active') }));
+buf.push('>Standard</a><a');
+buf.push(attrs({ 'href':("#"), 'data-filter':(""), "class": ('advanced-filter-button') + ' ' + ('options-filter-button') + ' ' + ('btn') }));
+buf.push('>Advanced</a>\n    </div>\n  </div>\n  <div');
+buf.push(attrs({ 'data-subview':("ChartOptionView"), "class": ('fields') + ' ' + ('isotope') }));
+buf.push('>\n  </div>\n</form>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/chart/chart-scaffold.jade.mod.js b/lib/template/chart/chart-scaffold.jade.mod.js
new file mode 100644 (file)
index 0000000..abf742f
--- /dev/null
@@ -0,0 +1,32 @@
+require.define('/node_modules/kraken/template/chart/chart-scaffold.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<form');
+buf.push(attrs({ "class": ('chart-options') + ' ' + ('scaffold') + ' ' + ('form-inline') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('chart-options-controls') + ' ' + ('control-group') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('collapse-all-options-button') + ' ' + ('btn') }));
+buf.push('>Collapse All</a><a');
+buf.push(attrs({ 'href':("#"), "class": ('expand-all-options-button') + ' ' + ('btn') }));
+buf.push('>Expand All</a>\n    <div');
+buf.push(attrs({ 'data-toggle':("buttons-radio"), "class": ('std-adv-filter-buttons') + ' ' + ('btn-group') + ' ' + ('pull-right') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), 'data-filter':(".tag_standard"), "class": ('standard-filter-button') + ' ' + ('options-filter-button') + ' ' + ('btn') + ' ' + ('active') }));
+buf.push('>Standard</a><a');
+buf.push(attrs({ 'href':("#"), 'data-filter':(""), "class": ('advanced-filter-button') + ' ' + ('options-filter-button') + ' ' + ('btn') }));
+buf.push('>Advanced</a>\n    </div>\n  </div>\n  <div');
+buf.push(attrs({ 'data-subview':("ChartOptionView"), "class": ('fields') + ' ' + ('isotope') }));
+buf.push('>\n  </div>\n</form>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/dashboard/dashboard-tab.jade.js b/lib/template/dashboard/dashboard-tab.jade.js
new file mode 100644 (file)
index 0000000..3fe6330
--- /dev/null
@@ -0,0 +1,14 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<div');
+buf.push(attrs({ 'id':(tab_id), 'data-subview':("GraphDisplayView"), "class": ('tab-pane') + ' ' + (tab_cls) }));
+buf.push('>\n</div>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/dashboard/dashboard-tab.jade.mod.js b/lib/template/dashboard/dashboard-tab.jade.mod.js
new file mode 100644 (file)
index 0000000..36f1df7
--- /dev/null
@@ -0,0 +1,18 @@
+require.define('/node_modules/kraken/template/dashboard/dashboard-tab.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<div');
+buf.push(attrs({ 'id':(tab_id), 'data-subview':("GraphDisplayView"), "class": ('tab-pane') + ' ' + (tab_cls) }));
+buf.push('>\n</div>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/dashboard/dashboard.jade.js b/lib/template/dashboard/dashboard.jade.js
new file mode 100644 (file)
index 0000000..afa3059
--- /dev/null
@@ -0,0 +1,24 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ 'id':('dashboard'), "class": ('centered') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('page-header') }));
+buf.push('>\n    <h1>Wikimedia Report Card <small>April 2012</small>\n    </h1>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('row') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('graphs') + ' ' + ('tabbable') }));
+buf.push('>\n      <nav>\n        <ul');
+buf.push(attrs({ "class": ('nav') + ' ' + ('subnav') + ' ' + ('nav-pills') }));
+buf.push('>\n          <li>\n            <h3>Graphs\n            </h3>\n          </li>\n        </ul>\n      </nav>\n      <div');
+buf.push(attrs({ 'data-subview':("DashboardTabView"), "class": ('tab-content') }));
+buf.push('>\n      </div>\n    </div>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/dashboard/dashboard.jade.mod.js b/lib/template/dashboard/dashboard.jade.mod.js
new file mode 100644 (file)
index 0000000..0e58d87
--- /dev/null
@@ -0,0 +1,28 @@
+require.define('/node_modules/kraken/template/dashboard/dashboard.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ 'id':('dashboard'), "class": ('centered') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('page-header') }));
+buf.push('>\n    <h1>Wikimedia Report Card <small>April 2012</small>\n    </h1>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('row') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('graphs') + ' ' + ('tabbable') }));
+buf.push('>\n      <nav>\n        <ul');
+buf.push(attrs({ "class": ('nav') + ' ' + ('subnav') + ' ' + ('nav-pills') }));
+buf.push('>\n          <li>\n            <h3>Graphs\n            </h3>\n          </li>\n        </ul>\n      </nav>\n      <div');
+buf.push(attrs({ 'data-subview':("DashboardTabView"), "class": ('tab-content') }));
+buf.push('>\n      </div>\n    </div>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/data/data.jade.js b/lib/template/data/data.jade.js
new file mode 100644 (file)
index 0000000..7b270cd
--- /dev/null
@@ -0,0 +1,20 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ "class": ('data-ui') }));
+buf.push('>\n  <form');
+buf.push(attrs({ "class": ('data-ui-form') }));
+buf.push('>\n    <div');
+buf.push(attrs({ 'data-subview':("MetricEditView"), "class": ('metric_edit_view_pane') }));
+buf.push('>\n    </div>\n    <div');
+buf.push(attrs({ 'data-subview':("DataSetView"), "class": ('data_set_view_pane') }));
+buf.push('>\n    </div>\n  </form>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/data/data.jade.mod.js b/lib/template/data/data.jade.mod.js
new file mode 100644 (file)
index 0000000..b15812d
--- /dev/null
@@ -0,0 +1,24 @@
+require.define('/node_modules/kraken/template/data/data.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ "class": ('data-ui') }));
+buf.push('>\n  <form');
+buf.push(attrs({ "class": ('data-ui-form') }));
+buf.push('>\n    <div');
+buf.push(attrs({ 'data-subview':("MetricEditView"), "class": ('metric_edit_view_pane') }));
+buf.push('>\n    </div>\n    <div');
+buf.push(attrs({ 'data-subview':("DataSetView"), "class": ('data_set_view_pane') }));
+buf.push('>\n    </div>\n  </form>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/data/dataset-metric.jade.js b/lib/template/data/dataset-metric.jade.js
new file mode 100644 (file)
index 0000000..8e2b249
--- /dev/null
@@ -0,0 +1,28 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<tr');
+buf.push(attrs({ "class": ('dataset-metric') + ' ' + (viewClasses) }));
+buf.push('>\n  <td');
+buf.push(attrs({ 'style':("color: " + (color) + ";"), 'data-bind':("label"), "class": ('col-label') + ' ' + ('col-color') }));
+buf.push('>' + escape((interp = label) == null ? '' : interp) + '\n  </td>\n  <td');
+buf.push(attrs({ 'data-bind':("source"), "class": ('col-source') }));
+buf.push('>' + escape((interp = source) == null ? '' : interp) + '\n  </td>\n  <td');
+buf.push(attrs({ 'data-bind':("timespan"), 'data-bind-escape':("false"), "class": ('col-time') }));
+buf.push('>' + escape((interp = timespan) == null ? '' : interp) + '\n  </td>\n  <td');
+buf.push(attrs({ "class": ('col-actions') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('delete-metric-button') + ' ' + ('control') + ' ' + ('close') }));
+buf.push('>&times;</a>\n    <div');
+buf.push(attrs({ "class": ('activity-arrow') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('inner') }));
+buf.push('>\n      </div>\n    </div>\n  </td>\n</tr>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/data/dataset-metric.jade.mod.js b/lib/template/data/dataset-metric.jade.mod.js
new file mode 100644 (file)
index 0000000..e36e2c5
--- /dev/null
@@ -0,0 +1,32 @@
+require.define('/node_modules/kraken/template/data/dataset-metric.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<tr');
+buf.push(attrs({ "class": ('dataset-metric') + ' ' + (viewClasses) }));
+buf.push('>\n  <td');
+buf.push(attrs({ 'style':("color: " + (color) + ";"), 'data-bind':("label"), "class": ('col-label') + ' ' + ('col-color') }));
+buf.push('>' + escape((interp = label) == null ? '' : interp) + '\n  </td>\n  <td');
+buf.push(attrs({ 'data-bind':("source"), "class": ('col-source') }));
+buf.push('>' + escape((interp = source) == null ? '' : interp) + '\n  </td>\n  <td');
+buf.push(attrs({ 'data-bind':("timespan"), 'data-bind-escape':("false"), "class": ('col-time') }));
+buf.push('>' + escape((interp = timespan) == null ? '' : interp) + '\n  </td>\n  <td');
+buf.push(attrs({ "class": ('col-actions') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('delete-metric-button') + ' ' + ('control') + ' ' + ('close') }));
+buf.push('>&times;</a>\n    <div');
+buf.push(attrs({ "class": ('activity-arrow') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('inner') }));
+buf.push('>\n      </div>\n    </div>\n  </td>\n</tr>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/data/dataset.jade.js b/lib/template/data/dataset.jade.js
new file mode 100644 (file)
index 0000000..1fab1fa
--- /dev/null
@@ -0,0 +1,38 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ "class": ('dataset-ui') + ' ' + ('dataset') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('inner') }));
+buf.push('>\n    <form');
+buf.push(attrs({ "class": ('dataset-ui-form') }));
+buf.push('>\n      <h4>Graph Data Set\n      </h4>\n      <div');
+buf.push(attrs({ "class": ('dataset-buttons') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('new-metric-button') + ' ' + ('btn') + ' ' + ('btn-success') + ' ' + ('btn-small') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-plus-sign') + ' ' + ('icon-white') }));
+buf.push('></i> Add Metric\n</a>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('dataset-metrics') }));
+buf.push('>\n        <table');
+buf.push(attrs({ "class": ('table') + ' ' + ('table-striped') }));
+buf.push('>\n          <thead>\n            <tr>\n              <th');
+buf.push(attrs({ "class": ('col-label') }));
+buf.push('>Label\n              </th>\n              <th');
+buf.push(attrs({ "class": ('col-source') }));
+buf.push('>Source\n              </th>\n              <th');
+buf.push(attrs({ "class": ('col-times') }));
+buf.push('>Timespan\n              </th>\n              <th');
+buf.push(attrs({ "class": ('col-actions') }));
+buf.push('>Actions\n              </th>\n            </tr>\n          </thead>\n          <tbody');
+buf.push(attrs({ 'data-subview':("DataSetMetricView"), "class": ('metrics') }));
+buf.push('>\n          </tbody>\n        </table>\n      </div>\n    </form>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/data/dataset.jade.mod.js b/lib/template/data/dataset.jade.mod.js
new file mode 100644 (file)
index 0000000..ff5a992
--- /dev/null
@@ -0,0 +1,42 @@
+require.define('/node_modules/kraken/template/data/dataset.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ "class": ('dataset-ui') + ' ' + ('dataset') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('inner') }));
+buf.push('>\n    <form');
+buf.push(attrs({ "class": ('dataset-ui-form') }));
+buf.push('>\n      <h4>Graph Data Set\n      </h4>\n      <div');
+buf.push(attrs({ "class": ('dataset-buttons') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('new-metric-button') + ' ' + ('btn') + ' ' + ('btn-success') + ' ' + ('btn-small') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-plus-sign') + ' ' + ('icon-white') }));
+buf.push('></i> Add Metric\n</a>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('dataset-metrics') }));
+buf.push('>\n        <table');
+buf.push(attrs({ "class": ('table') + ' ' + ('table-striped') }));
+buf.push('>\n          <thead>\n            <tr>\n              <th');
+buf.push(attrs({ "class": ('col-label') }));
+buf.push('>Label\n              </th>\n              <th');
+buf.push(attrs({ "class": ('col-source') }));
+buf.push('>Source\n              </th>\n              <th');
+buf.push(attrs({ "class": ('col-times') }));
+buf.push('>Timespan\n              </th>\n              <th');
+buf.push(attrs({ "class": ('col-actions') }));
+buf.push('>Actions\n              </th>\n            </tr>\n          </thead>\n          <tbody');
+buf.push(attrs({ 'data-subview':("DataSetMetricView"), "class": ('metrics') }));
+buf.push('>\n          </tbody>\n        </table>\n      </div>\n    </form>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/data/datasource-ui.jade.js b/lib/template/data/datasource-ui.jade.js
new file mode 100644 (file)
index 0000000..a8c8180
--- /dev/null
@@ -0,0 +1,232 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ "class": ('datasource-ui') + ' ' + ("datasource-ui-" + (cid) + "") }));
+buf.push('>\n  <form');
+buf.push(attrs({ "class": ('datasource-ui-form') }));
+buf.push('>\n    <section');
+buf.push(attrs({ 'data-toggle':("collapse"), 'data-target':("#" + (graph_id) + " .datasource-ui.datasource-ui-" + (cid) + " .datasource-selector"), "class": ('datasource-summary') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('expand-datasource-ui-button') + ' ' + ('icon-chevron-down') }));
+buf.push('></i><i');
+buf.push(attrs({ "class": ('collapse-datasource-ui-button') + ' ' + ('icon-chevron-up') }));
+buf.push('></i>\n      <ul');
+buf.push(attrs({ "class": ('breadcrumb') }));
+buf.push('>\n        <li>' + escape((interp = source_summary) == null ? '' : interp) + ' <span');
+buf.push(attrs({ "class": ('divider') }));
+buf.push('>\/</span>\n        </li>\n        <li>' + escape((interp = metric_summary) == null ? '' : interp) + ' <span');
+buf.push(attrs({ "class": ('divider') }));
+buf.push('>\/</span>\n        </li>\n        <li>' + escape((interp = timespan_summary) == null ? '' : interp) + '\n        </li>\n      </ul>\n    </section>\n    <section');
+buf.push(attrs({ "class": ('datasource-selector') + ' ' + ('collapse') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('datasource-tabs') }));
+buf.push('>\n        <div');
+buf.push(attrs({ "class": ('tabbable') + ' ' + ('tabs-left') }));
+buf.push('>\n          <ul');
+buf.push(attrs({ "class": ('datasource-sources-list') + ' ' + ('nav') + ' ' + ('nav-tabs') }));
+buf.push('>\n            <li>\n              <h6>Data Sources\n              </h6>\n            </li>');
+// iterate datasources.models
+(function(){
+  if ('number' == typeof datasources.models.length) {
+    for (var k = 0, $$l = datasources.models.length; k < $$l; k++) {
+      var source = datasources.models[k];
+
+ var ds = source.attributes
+ var activeClass = (source_id === ds.id ? 'active' : '')
+ var ds_target = "#"+graph_id+" .datasource-ui .datasource-selector .datasource-source.datasource-source-"+ds.id
+buf.push('\n            <li');
+buf.push(attrs({ "class": (activeClass) }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#datasource-selector_datasource-source-" + (ds.id) + ""), 'data-toggle':("tab"), 'data-target':(ds_target) }));
+buf.push('>' + escape((interp = ds.shortName) == null ? '' : interp) + '</a>\n            </li>');
+    }
+  } else {
+    for (var k in datasources.models) {
+      var source = datasources.models[k];
+
+ var ds = source.attributes
+ var activeClass = (source_id === ds.id ? 'active' : '')
+ var ds_target = "#"+graph_id+" .datasource-ui .datasource-selector .datasource-source.datasource-source-"+ds.id
+buf.push('\n            <li');
+buf.push(attrs({ "class": (activeClass) }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#datasource-selector_datasource-source-" + (ds.id) + ""), 'data-toggle':("tab"), 'data-target':(ds_target) }));
+buf.push('>' + escape((interp = ds.shortName) == null ? '' : interp) + '</a>\n            </li>');
+   }
+  }
+}).call(this);
+
+buf.push('\n          </ul>\n          <div');
+buf.push(attrs({ "class": ('datasource-sources-info') + ' ' + ('tab-content') }));
+buf.push('>');
+// iterate datasources.models
+(function(){
+  if ('number' == typeof datasources.models.length) {
+    for (var k = 0, $$l = datasources.models.length; k < $$l; k++) {
+      var source = datasources.models[k];
+
+ var ds = source.attributes
+ var activeClass = (source_id === ds.id ? 'active' : '')
+buf.push('\n            <div');
+buf.push(attrs({ "class": ('datasource-source') + ' ' + ('tab-pane') + ' ' + ("datasource-source-" + (ds.id) + " " + (activeClass) + "") }));
+buf.push('>\n              <div');
+buf.push(attrs({ "class": ('datasource-source-details') + ' ' + ('well') }));
+buf.push('>\n                <div');
+buf.push(attrs({ "class": ('source-name') }));
+buf.push('>' + escape((interp = ds.name) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-id') }));
+buf.push('>' + escape((interp = ds.id) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-format') }));
+buf.push('>' + escape((interp = ds.format) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-charttype') }));
+buf.push('>' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n                </div>\n                <input');
+buf.push(attrs({ 'type':("text"), 'name':("source-url"), 'value':(ds.url), "class": ('source-url') }));
+buf.push('/>\n                <div');
+buf.push(attrs({ "class": ('datasource-source-time') }));
+buf.push('>\n                  <div');
+buf.push(attrs({ "class": ('source-time-start') }));
+buf.push('>' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n                  </div>\n                  <div');
+buf.push(attrs({ "class": ('source-time-end') }));
+buf.push('>' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n                  </div>\n                  <div');
+buf.push(attrs({ "class": ('source-time-step') }));
+buf.push('>' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n                  </div>\n                </div>\n              </div>\n              <div');
+buf.push(attrs({ "class": ('datasource-source-metrics') }));
+buf.push('>\n                <table');
+buf.push(attrs({ "class": ('table') + ' ' + ('table-striped') }));
+buf.push('>\n                  <thead>\n                    <tr>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>#\n                      </th>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>Label\n                      </th>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>Type\n                      </th>\n                    </tr>\n                  </thead>\n                  <tbody');
+buf.push(attrs({ "class": ('source-metrics') }));
+buf.push('>');
+// iterate ds.metrics.slice(1)
+(function(){
+  if ('number' == typeof ds.metrics.slice(1).length) {
+    for (var idx = 0, $$l = ds.metrics.slice(1).length; idx < $$l; idx++) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n                    <tr');
+buf.push(attrs({ 'data-source_id':(ds.id), 'data-source_col':(m.idx), "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n                      </td>\n                    </tr>');
+    }
+  } else {
+    for (var idx in ds.metrics.slice(1)) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n                    <tr');
+buf.push(attrs({ 'data-source_id':(ds.id), 'data-source_col':(m.idx), "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n                      </td>\n                    </tr>');
+   }
+  }
+}).call(this);
+
+buf.push('\n                  </tbody>\n                </table>\n              </div>\n            </div>');
+    }
+  } else {
+    for (var k in datasources.models) {
+      var source = datasources.models[k];
+
+ var ds = source.attributes
+ var activeClass = (source_id === ds.id ? 'active' : '')
+buf.push('\n            <div');
+buf.push(attrs({ "class": ('datasource-source') + ' ' + ('tab-pane') + ' ' + ("datasource-source-" + (ds.id) + " " + (activeClass) + "") }));
+buf.push('>\n              <div');
+buf.push(attrs({ "class": ('datasource-source-details') + ' ' + ('well') }));
+buf.push('>\n                <div');
+buf.push(attrs({ "class": ('source-name') }));
+buf.push('>' + escape((interp = ds.name) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-id') }));
+buf.push('>' + escape((interp = ds.id) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-format') }));
+buf.push('>' + escape((interp = ds.format) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-charttype') }));
+buf.push('>' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n                </div>\n                <input');
+buf.push(attrs({ 'type':("text"), 'name':("source-url"), 'value':(ds.url), "class": ('source-url') }));
+buf.push('/>\n                <div');
+buf.push(attrs({ "class": ('datasource-source-time') }));
+buf.push('>\n                  <div');
+buf.push(attrs({ "class": ('source-time-start') }));
+buf.push('>' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n                  </div>\n                  <div');
+buf.push(attrs({ "class": ('source-time-end') }));
+buf.push('>' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n                  </div>\n                  <div');
+buf.push(attrs({ "class": ('source-time-step') }));
+buf.push('>' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n                  </div>\n                </div>\n              </div>\n              <div');
+buf.push(attrs({ "class": ('datasource-source-metrics') }));
+buf.push('>\n                <table');
+buf.push(attrs({ "class": ('table') + ' ' + ('table-striped') }));
+buf.push('>\n                  <thead>\n                    <tr>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>#\n                      </th>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>Label\n                      </th>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>Type\n                      </th>\n                    </tr>\n                  </thead>\n                  <tbody');
+buf.push(attrs({ "class": ('source-metrics') }));
+buf.push('>');
+// iterate ds.metrics.slice(1)
+(function(){
+  if ('number' == typeof ds.metrics.slice(1).length) {
+    for (var idx = 0, $$l = ds.metrics.slice(1).length; idx < $$l; idx++) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n                    <tr');
+buf.push(attrs({ 'data-source_id':(ds.id), 'data-source_col':(m.idx), "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n                      </td>\n                    </tr>');
+    }
+  } else {
+    for (var idx in ds.metrics.slice(1)) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n                    <tr');
+buf.push(attrs({ 'data-source_id':(ds.id), 'data-source_col':(m.idx), "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n                      </td>\n                    </tr>');
+   }
+  }
+}).call(this);
+
+buf.push('\n                  </tbody>\n                </table>\n              </div>\n            </div>');
+   }
+  }
+}).call(this);
+
+buf.push('\n          </div>\n        </div>\n      </div>\n    </section>\n  </form>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/data/datasource-ui.jade.mod.js b/lib/template/data/datasource-ui.jade.mod.js
new file mode 100644 (file)
index 0000000..b25e11e
--- /dev/null
@@ -0,0 +1,236 @@
+require.define('/node_modules/kraken/template/data/datasource-ui.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ "class": ('datasource-ui') + ' ' + ("datasource-ui-" + (cid) + "") }));
+buf.push('>\n  <form');
+buf.push(attrs({ "class": ('datasource-ui-form') }));
+buf.push('>\n    <section');
+buf.push(attrs({ 'data-toggle':("collapse"), 'data-target':("#" + (graph_id) + " .datasource-ui.datasource-ui-" + (cid) + " .datasource-selector"), "class": ('datasource-summary') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('expand-datasource-ui-button') + ' ' + ('icon-chevron-down') }));
+buf.push('></i><i');
+buf.push(attrs({ "class": ('collapse-datasource-ui-button') + ' ' + ('icon-chevron-up') }));
+buf.push('></i>\n      <ul');
+buf.push(attrs({ "class": ('breadcrumb') }));
+buf.push('>\n        <li>' + escape((interp = source_summary) == null ? '' : interp) + ' <span');
+buf.push(attrs({ "class": ('divider') }));
+buf.push('>\/</span>\n        </li>\n        <li>' + escape((interp = metric_summary) == null ? '' : interp) + ' <span');
+buf.push(attrs({ "class": ('divider') }));
+buf.push('>\/</span>\n        </li>\n        <li>' + escape((interp = timespan_summary) == null ? '' : interp) + '\n        </li>\n      </ul>\n    </section>\n    <section');
+buf.push(attrs({ "class": ('datasource-selector') + ' ' + ('collapse') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('datasource-tabs') }));
+buf.push('>\n        <div');
+buf.push(attrs({ "class": ('tabbable') + ' ' + ('tabs-left') }));
+buf.push('>\n          <ul');
+buf.push(attrs({ "class": ('datasource-sources-list') + ' ' + ('nav') + ' ' + ('nav-tabs') }));
+buf.push('>\n            <li>\n              <h6>Data Sources\n              </h6>\n            </li>');
+// iterate datasources.models
+(function(){
+  if ('number' == typeof datasources.models.length) {
+    for (var k = 0, $$l = datasources.models.length; k < $$l; k++) {
+      var source = datasources.models[k];
+
+ var ds = source.attributes
+ var activeClass = (source_id === ds.id ? 'active' : '')
+ var ds_target = "#"+graph_id+" .datasource-ui .datasource-selector .datasource-source.datasource-source-"+ds.id
+buf.push('\n            <li');
+buf.push(attrs({ "class": (activeClass) }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#datasource-selector_datasource-source-" + (ds.id) + ""), 'data-toggle':("tab"), 'data-target':(ds_target) }));
+buf.push('>' + escape((interp = ds.shortName) == null ? '' : interp) + '</a>\n            </li>');
+    }
+  } else {
+    for (var k in datasources.models) {
+      var source = datasources.models[k];
+
+ var ds = source.attributes
+ var activeClass = (source_id === ds.id ? 'active' : '')
+ var ds_target = "#"+graph_id+" .datasource-ui .datasource-selector .datasource-source.datasource-source-"+ds.id
+buf.push('\n            <li');
+buf.push(attrs({ "class": (activeClass) }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#datasource-selector_datasource-source-" + (ds.id) + ""), 'data-toggle':("tab"), 'data-target':(ds_target) }));
+buf.push('>' + escape((interp = ds.shortName) == null ? '' : interp) + '</a>\n            </li>');
+   }
+  }
+}).call(this);
+
+buf.push('\n          </ul>\n          <div');
+buf.push(attrs({ "class": ('datasource-sources-info') + ' ' + ('tab-content') }));
+buf.push('>');
+// iterate datasources.models
+(function(){
+  if ('number' == typeof datasources.models.length) {
+    for (var k = 0, $$l = datasources.models.length; k < $$l; k++) {
+      var source = datasources.models[k];
+
+ var ds = source.attributes
+ var activeClass = (source_id === ds.id ? 'active' : '')
+buf.push('\n            <div');
+buf.push(attrs({ "class": ('datasource-source') + ' ' + ('tab-pane') + ' ' + ("datasource-source-" + (ds.id) + " " + (activeClass) + "") }));
+buf.push('>\n              <div');
+buf.push(attrs({ "class": ('datasource-source-details') + ' ' + ('well') }));
+buf.push('>\n                <div');
+buf.push(attrs({ "class": ('source-name') }));
+buf.push('>' + escape((interp = ds.name) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-id') }));
+buf.push('>' + escape((interp = ds.id) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-format') }));
+buf.push('>' + escape((interp = ds.format) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-charttype') }));
+buf.push('>' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n                </div>\n                <input');
+buf.push(attrs({ 'type':("text"), 'name':("source-url"), 'value':(ds.url), "class": ('source-url') }));
+buf.push('/>\n                <div');
+buf.push(attrs({ "class": ('datasource-source-time') }));
+buf.push('>\n                  <div');
+buf.push(attrs({ "class": ('source-time-start') }));
+buf.push('>' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n                  </div>\n                  <div');
+buf.push(attrs({ "class": ('source-time-end') }));
+buf.push('>' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n                  </div>\n                  <div');
+buf.push(attrs({ "class": ('source-time-step') }));
+buf.push('>' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n                  </div>\n                </div>\n              </div>\n              <div');
+buf.push(attrs({ "class": ('datasource-source-metrics') }));
+buf.push('>\n                <table');
+buf.push(attrs({ "class": ('table') + ' ' + ('table-striped') }));
+buf.push('>\n                  <thead>\n                    <tr>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>#\n                      </th>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>Label\n                      </th>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>Type\n                      </th>\n                    </tr>\n                  </thead>\n                  <tbody');
+buf.push(attrs({ "class": ('source-metrics') }));
+buf.push('>');
+// iterate ds.metrics.slice(1)
+(function(){
+  if ('number' == typeof ds.metrics.slice(1).length) {
+    for (var idx = 0, $$l = ds.metrics.slice(1).length; idx < $$l; idx++) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n                    <tr');
+buf.push(attrs({ 'data-source_id':(ds.id), 'data-source_col':(m.idx), "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n                      </td>\n                    </tr>');
+    }
+  } else {
+    for (var idx in ds.metrics.slice(1)) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n                    <tr');
+buf.push(attrs({ 'data-source_id':(ds.id), 'data-source_col':(m.idx), "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n                      </td>\n                    </tr>');
+   }
+  }
+}).call(this);
+
+buf.push('\n                  </tbody>\n                </table>\n              </div>\n            </div>');
+    }
+  } else {
+    for (var k in datasources.models) {
+      var source = datasources.models[k];
+
+ var ds = source.attributes
+ var activeClass = (source_id === ds.id ? 'active' : '')
+buf.push('\n            <div');
+buf.push(attrs({ "class": ('datasource-source') + ' ' + ('tab-pane') + ' ' + ("datasource-source-" + (ds.id) + " " + (activeClass) + "") }));
+buf.push('>\n              <div');
+buf.push(attrs({ "class": ('datasource-source-details') + ' ' + ('well') }));
+buf.push('>\n                <div');
+buf.push(attrs({ "class": ('source-name') }));
+buf.push('>' + escape((interp = ds.name) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-id') }));
+buf.push('>' + escape((interp = ds.id) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-format') }));
+buf.push('>' + escape((interp = ds.format) == null ? '' : interp) + '\n                </div>\n                <div');
+buf.push(attrs({ "class": ('source-charttype') }));
+buf.push('>' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n                </div>\n                <input');
+buf.push(attrs({ 'type':("text"), 'name':("source-url"), 'value':(ds.url), "class": ('source-url') }));
+buf.push('/>\n                <div');
+buf.push(attrs({ "class": ('datasource-source-time') }));
+buf.push('>\n                  <div');
+buf.push(attrs({ "class": ('source-time-start') }));
+buf.push('>' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n                  </div>\n                  <div');
+buf.push(attrs({ "class": ('source-time-end') }));
+buf.push('>' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n                  </div>\n                  <div');
+buf.push(attrs({ "class": ('source-time-step') }));
+buf.push('>' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n                  </div>\n                </div>\n              </div>\n              <div');
+buf.push(attrs({ "class": ('datasource-source-metrics') }));
+buf.push('>\n                <table');
+buf.push(attrs({ "class": ('table') + ' ' + ('table-striped') }));
+buf.push('>\n                  <thead>\n                    <tr>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>#\n                      </th>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>Label\n                      </th>\n                      <th');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>Type\n                      </th>\n                    </tr>\n                  </thead>\n                  <tbody');
+buf.push(attrs({ "class": ('source-metrics') }));
+buf.push('>');
+// iterate ds.metrics.slice(1)
+(function(){
+  if ('number' == typeof ds.metrics.slice(1).length) {
+    for (var idx = 0, $$l = ds.metrics.slice(1).length; idx < $$l; idx++) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n                    <tr');
+buf.push(attrs({ 'data-source_id':(ds.id), 'data-source_col':(m.idx), "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n                      </td>\n                    </tr>');
+    }
+  } else {
+    for (var idx in ds.metrics.slice(1)) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n                    <tr');
+buf.push(attrs({ 'data-source_id':(ds.id), 'data-source_col':(m.idx), "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n                      </td>\n                      <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n                      </td>\n                    </tr>');
+   }
+  }
+}).call(this);
+
+buf.push('\n                  </tbody>\n                </table>\n              </div>\n            </div>');
+   }
+  }
+}).call(this);
+
+buf.push('\n          </div>\n        </div>\n      </div>\n    </section>\n  </form>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/data/datasource.jade.js b/lib/template/data/datasource.jade.js
new file mode 100644 (file)
index 0000000..3e52647
--- /dev/null
@@ -0,0 +1,87 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ var ds = source.attributes
+ var id = ds.id || source.cid
+ var activeClass = (source_id === ds.id ? 'active' : '')
+buf.push('\n<div');
+buf.push(attrs({ "class": ('datasource-source') + ' ' + ('tab-pane') + ' ' + ("datasource-source-" + (id) + " " + (activeClass) + "") }));
+buf.push('>\n  <form');
+buf.push(attrs({ "class": ('datasource-source-form') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('datasource-source-details') + ' ' + ('well') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('source-name') }));
+buf.push('>' + escape((interp = ds.name) == null ? '' : interp) + '\n      </div>\n      <div');
+buf.push(attrs({ "class": ('source-id') }));
+buf.push('>' + escape((interp = ds.id) == null ? '' : interp) + '\n      </div>\n      <div');
+buf.push(attrs({ "class": ('source-format') }));
+buf.push('>' + escape((interp = ds.format) == null ? '' : interp) + '\n      </div>\n      <div');
+buf.push(attrs({ "class": ('source-charttype') }));
+buf.push('>' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n      </div>\n      <input');
+buf.push(attrs({ 'type':("text"), 'name':("source-url"), 'value':(ds.url), "class": ('source-url') }));
+buf.push('/>\n      <div');
+buf.push(attrs({ "class": ('datasource-source-time') }));
+buf.push('>\n        <div');
+buf.push(attrs({ "class": ('source-time-start') }));
+buf.push('>' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n        </div>\n        <div');
+buf.push(attrs({ "class": ('source-time-end') }));
+buf.push('>' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n        </div>\n        <div');
+buf.push(attrs({ "class": ('source-time-step') }));
+buf.push('>' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n        </div>\n      </div>\n    </div>\n    <div');
+buf.push(attrs({ "class": ('datasource-source-metrics') }));
+buf.push('>\n      <table');
+buf.push(attrs({ "class": ('table') + ' ' + ('table-striped') }));
+buf.push('>\n        <thead>\n          <tr>\n            <th');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>#\n            </th>\n            <th');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>Label\n            </th>\n            <th');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>Type\n            </th>\n          </tr>\n        </thead>\n        <tbody');
+buf.push(attrs({ "class": ('source-metrics') }));
+buf.push('>');
+// iterate ds.metrics.slice(1)
+(function(){
+  if ('number' == typeof ds.metrics.slice(1).length) {
+    for (var idx = 0, $$l = ds.metrics.slice(1).length; idx < $$l; idx++) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n          <tr');
+buf.push(attrs({ "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n            <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n            </td>\n            <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n            </td>\n            <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n            </td>\n          </tr>');
+    }
+  } else {
+    for (var idx in ds.metrics.slice(1)) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n          <tr');
+buf.push(attrs({ "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n            <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n            </td>\n            <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n            </td>\n            <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n            </td>\n          </tr>');
+   }
+  }
+}).call(this);
+
+buf.push('\n        </tbody>\n      </table>\n    </div>\n  </form>\n</div>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/data/datasource.jade.mod.js b/lib/template/data/datasource.jade.mod.js
new file mode 100644 (file)
index 0000000..9f14057
--- /dev/null
@@ -0,0 +1,91 @@
+require.define('/node_modules/kraken/template/data/datasource.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ var ds = source.attributes
+ var id = ds.id || source.cid
+ var activeClass = (source_id === ds.id ? 'active' : '')
+buf.push('\n<div');
+buf.push(attrs({ "class": ('datasource-source') + ' ' + ('tab-pane') + ' ' + ("datasource-source-" + (id) + " " + (activeClass) + "") }));
+buf.push('>\n  <form');
+buf.push(attrs({ "class": ('datasource-source-form') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('datasource-source-details') + ' ' + ('well') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('source-name') }));
+buf.push('>' + escape((interp = ds.name) == null ? '' : interp) + '\n      </div>\n      <div');
+buf.push(attrs({ "class": ('source-id') }));
+buf.push('>' + escape((interp = ds.id) == null ? '' : interp) + '\n      </div>\n      <div');
+buf.push(attrs({ "class": ('source-format') }));
+buf.push('>' + escape((interp = ds.format) == null ? '' : interp) + '\n      </div>\n      <div');
+buf.push(attrs({ "class": ('source-charttype') }));
+buf.push('>' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n      </div>\n      <input');
+buf.push(attrs({ 'type':("text"), 'name':("source-url"), 'value':(ds.url), "class": ('source-url') }));
+buf.push('/>\n      <div');
+buf.push(attrs({ "class": ('datasource-source-time') }));
+buf.push('>\n        <div');
+buf.push(attrs({ "class": ('source-time-start') }));
+buf.push('>' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n        </div>\n        <div');
+buf.push(attrs({ "class": ('source-time-end') }));
+buf.push('>' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n        </div>\n        <div');
+buf.push(attrs({ "class": ('source-time-step') }));
+buf.push('>' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n        </div>\n      </div>\n    </div>\n    <div');
+buf.push(attrs({ "class": ('datasource-source-metrics') }));
+buf.push('>\n      <table');
+buf.push(attrs({ "class": ('table') + ' ' + ('table-striped') }));
+buf.push('>\n        <thead>\n          <tr>\n            <th');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>#\n            </th>\n            <th');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>Label\n            </th>\n            <th');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>Type\n            </th>\n          </tr>\n        </thead>\n        <tbody');
+buf.push(attrs({ "class": ('source-metrics') }));
+buf.push('>');
+// iterate ds.metrics.slice(1)
+(function(){
+  if ('number' == typeof ds.metrics.slice(1).length) {
+    for (var idx = 0, $$l = ds.metrics.slice(1).length; idx < $$l; idx++) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n          <tr');
+buf.push(attrs({ "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n            <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n            </td>\n            <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n            </td>\n            <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n            </td>\n          </tr>');
+    }
+  } else {
+    for (var idx in ds.metrics.slice(1)) {
+      var m = ds.metrics.slice(1)[idx];
+
+ var activeColClass = (activeClass && source_col === m.idx) ? 'active' : ''
+buf.push('\n          <tr');
+buf.push(attrs({ "class": ('datasource-source-metric') + ' ' + (activeColClass) }));
+buf.push('>\n            <td');
+buf.push(attrs({ "class": ('source-metric-idx') }));
+buf.push('>' + escape((interp = m.idx) == null ? '' : interp) + '\n            </td>\n            <td');
+buf.push(attrs({ "class": ('source-metric-label') }));
+buf.push('>' + escape((interp = m.label) == null ? '' : interp) + '\n            </td>\n            <td');
+buf.push(attrs({ "class": ('source-metric-type') }));
+buf.push('>' + escape((interp = m.type) == null ? '' : interp) + '\n            </td>\n          </tr>');
+   }
+  }
+}).call(this);
+
+buf.push('\n        </tbody>\n      </table>\n    </div>\n  </form>\n</div>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/data/metric-edit.jade.js b/lib/template/data/metric-edit.jade.js
new file mode 100644 (file)
index 0000000..32ef6c8
--- /dev/null
@@ -0,0 +1,40 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ "class": ('metric-edit-ui') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('inner') }));
+buf.push('>\n    <form');
+buf.push(attrs({ "class": ('metric-edit-form') + ' ' + ('form-horizontal') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('metric-header') + ' ' + ('control-group') }));
+buf.push('>\n        <div');
+buf.push(attrs({ 'data-color':("" + (color) + ""), 'data-color-format':("hex"), "class": ('color-swatch') + ' ' + ('input-append') + ' ' + ('color') }));
+buf.push('>\n          <input');
+buf.push(attrs({ 'type':('hidden'), 'id':("" + (graph_id) + "_metric_color"), 'name':("color"), 'value':(color), "class": ('metric-color') }));
+buf.push('/><span');
+buf.push(attrs({ "class": ('add-on') }));
+buf.push('><i');
+buf.push(attrs({ 'style':("background-color: " + (color) + ";"), 'title':("" + (color) + "") }));
+buf.push('></i></span>\n        </div>\n        <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_metric_label"), 'name':('label'), 'placeholder':('' + (placeholder_label) + ''), 'value':(label), "class": ('metric-label') }));
+buf.push('/>\n      </div>\n      <div');
+buf.push(attrs({ 'data-subview':("DataSourceUIView"), "class": ('metric-datasource') + ' ' + ('control-group') }));
+buf.push('>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('metric-actions') + ' ' + ('control-group') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('delete-button') + ' ' + ('btn') + ' ' + ('btn-danger') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-remove') + ' ' + ('icon-white') }));
+buf.push('></i> Delete\n</a>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('metric-options') + ' ' + ('row-fluid') }));
+buf.push('>\n      </div>\n    </form>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/data/metric-edit.jade.mod.js b/lib/template/data/metric-edit.jade.mod.js
new file mode 100644 (file)
index 0000000..95fe254
--- /dev/null
@@ -0,0 +1,44 @@
+require.define('/node_modules/kraken/template/data/metric-edit.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ "class": ('metric-edit-ui') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('inner') }));
+buf.push('>\n    <form');
+buf.push(attrs({ "class": ('metric-edit-form') + ' ' + ('form-horizontal') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('metric-header') + ' ' + ('control-group') }));
+buf.push('>\n        <div');
+buf.push(attrs({ 'data-color':("" + (color) + ""), 'data-color-format':("hex"), "class": ('color-swatch') + ' ' + ('input-append') + ' ' + ('color') }));
+buf.push('>\n          <input');
+buf.push(attrs({ 'type':('hidden'), 'id':("" + (graph_id) + "_metric_color"), 'name':("color"), 'value':(color), "class": ('metric-color') }));
+buf.push('/><span');
+buf.push(attrs({ "class": ('add-on') }));
+buf.push('><i');
+buf.push(attrs({ 'style':("background-color: " + (color) + ";"), 'title':("" + (color) + "") }));
+buf.push('></i></span>\n        </div>\n        <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_metric_label"), 'name':('label'), 'placeholder':('' + (placeholder_label) + ''), 'value':(label), "class": ('metric-label') }));
+buf.push('/>\n      </div>\n      <div');
+buf.push(attrs({ 'data-subview':("DataSourceUIView"), "class": ('metric-datasource') + ' ' + ('control-group') }));
+buf.push('>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('metric-actions') + ' ' + ('control-group') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('delete-button') + ' ' + ('btn') + ' ' + ('btn-danger') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-remove') + ' ' + ('icon-white') }));
+buf.push('></i> Delete\n</a>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('metric-options') + ' ' + ('row-fluid') }));
+buf.push('>\n      </div>\n    </form>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/graph/graph-display.jade.js b/lib/template/graph/graph-display.jade.js
new file mode 100644 (file)
index 0000000..f32ee08
--- /dev/null
@@ -0,0 +1,74 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ window.Markdown || (window.Markdown = new (require('showdown').Showdown).converter());
+ (jade.filters || (jade.filters = {})).markdown = function (s, name){ return s && Markdown.makeHtml(s.replace(/\n/g, '\n\n')); };
+ var graph_id = view.id
+buf.push('\n<section');
+buf.push(attrs({ 'id':(graph_id), "class": ('graph-display') + ' ' + ('graph') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('graph-name-row') + ' ' + ('row-fluid') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('callout') }));
+buf.push('>\n      <div');
+buf.push(attrs({ 'data-bind':('callout.latest'), "class": ('latest-metric') }));
+buf.push('>' + escape((interp = callout.latest) == null ? '' : interp) + '\n      </div>\n      <div');
+buf.push(attrs({ "class": ('metric-change') + ' ' + ('year-over-year') }));
+buf.push('><span');
+buf.push(attrs({ 'data-bind':('callout.year.dates'), "class": ('dates') }));
+buf.push('>' + escape((interp = callout.year.dates) == null ? '' : interp) + '</span><span');
+buf.push(attrs({ 'data-bind':('callout.year.value'), "class": ('value') }));
+buf.push('>' + escape((interp = callout.year.value) == null ? '' : interp) + '</span>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('metric-change') + ' ' + ('month-over-month') }));
+buf.push('><span');
+buf.push(attrs({ 'data-bind':('callout.month.dates'), "class": ('dates') }));
+buf.push('>' + escape((interp = callout.month.dates) == null ? '' : interp) + '</span><span');
+buf.push(attrs({ 'data-bind':('callout.month.value'), "class": ('value') }));
+buf.push('>' + escape((interp = callout.month.value) == null ? '' : interp) + '</span>\n      </div>\n    </div>\n    <h2><a');
+buf.push(attrs({ 'href':("" + (model.toLink()) + ""), 'data-bind':('name'), 'name':("" + (graph_id) + ""), "class": ('graph-name') }));
+buf.push('>' + escape((interp = name) == null ? '' : interp) + '</a>\n    </h2>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-viewport-row') + ' ' + ('row-fluid') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('inner') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('viewport') }));
+buf.push('>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('graph-legend') }));
+buf.push('>\n      </div>\n    </div>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-details-row') + ' ' + ('row') }));
+buf.push('>\n    <div');
+buf.push(attrs({ 'data-bind':('desc'), "class": ('span7') + ' ' + ('offset3') + ' ' + ('ug') + ' ' + ('graph-desc') }));
+buf.push('>');
+var __val__ = jade.filters.markdown(desc)
+buf.push(null == __val__ ? "" : __val__);
+buf.push('\n    </div>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-links-row') + ' ' + ('row') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('span6') + ' ' + ('offset3') + ' ' + ('ug') + ' ' + ('graph-permalink') }));
+buf.push('>\n      <input');
+buf.push(attrs({ 'value':("" + (model.toPermalink()) + ""), 'readonly':("readonly"), "class": ('span6') }));
+buf.push('/>\n    </div>');
+if ( IS_DEV)
+{
+buf.push('<a');
+buf.push(attrs({ 'href':("#"), "class": ('span1') + ' ' + ('export-button') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-file') }));
+buf.push('></i> Export\n</a>');
+}
+buf.push('\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-notes-row') + ' ' + ('row') }));
+buf.push('>\n    <div');
+buf.push(attrs({ 'data-bind':('notes'), "class": ('span7') + ' ' + ('offset3') + ' ' + ('ug') + ' ' + ('graph-notes') }));
+buf.push('>');
+var __val__ = jade.filters.markdown(notes)
+buf.push(null == __val__ ? "" : __val__);
+buf.push('\n    </div>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/graph/graph-display.jade.mod.js b/lib/template/graph/graph-display.jade.mod.js
new file mode 100644 (file)
index 0000000..baa4084
--- /dev/null
@@ -0,0 +1,78 @@
+require.define('/node_modules/kraken/template/graph/graph-display.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ window.Markdown || (window.Markdown = new (require('showdown').Showdown).converter());
+ (jade.filters || (jade.filters = {})).markdown = function (s, name){ return s && Markdown.makeHtml(s.replace(/\n/g, '\n\n')); };
+ var graph_id = view.id
+buf.push('\n<section');
+buf.push(attrs({ 'id':(graph_id), "class": ('graph-display') + ' ' + ('graph') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('graph-name-row') + ' ' + ('row-fluid') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('callout') }));
+buf.push('>\n      <div');
+buf.push(attrs({ 'data-bind':('callout.latest'), "class": ('latest-metric') }));
+buf.push('>' + escape((interp = callout.latest) == null ? '' : interp) + '\n      </div>\n      <div');
+buf.push(attrs({ "class": ('metric-change') + ' ' + ('year-over-year') }));
+buf.push('><span');
+buf.push(attrs({ 'data-bind':('callout.year.dates'), "class": ('dates') }));
+buf.push('>' + escape((interp = callout.year.dates) == null ? '' : interp) + '</span><span');
+buf.push(attrs({ 'data-bind':('callout.year.value'), "class": ('value') }));
+buf.push('>' + escape((interp = callout.year.value) == null ? '' : interp) + '</span>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('metric-change') + ' ' + ('month-over-month') }));
+buf.push('><span');
+buf.push(attrs({ 'data-bind':('callout.month.dates'), "class": ('dates') }));
+buf.push('>' + escape((interp = callout.month.dates) == null ? '' : interp) + '</span><span');
+buf.push(attrs({ 'data-bind':('callout.month.value'), "class": ('value') }));
+buf.push('>' + escape((interp = callout.month.value) == null ? '' : interp) + '</span>\n      </div>\n    </div>\n    <h2><a');
+buf.push(attrs({ 'href':("" + (model.toLink()) + ""), 'data-bind':('name'), 'name':("" + (graph_id) + ""), "class": ('graph-name') }));
+buf.push('>' + escape((interp = name) == null ? '' : interp) + '</a>\n    </h2>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-viewport-row') + ' ' + ('row-fluid') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('inner') }));
+buf.push('>\n      <div');
+buf.push(attrs({ "class": ('viewport') }));
+buf.push('>\n      </div>\n      <div');
+buf.push(attrs({ "class": ('graph-legend') }));
+buf.push('>\n      </div>\n    </div>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-details-row') + ' ' + ('row') }));
+buf.push('>\n    <div');
+buf.push(attrs({ 'data-bind':('desc'), "class": ('span7') + ' ' + ('offset3') + ' ' + ('ug') + ' ' + ('graph-desc') }));
+buf.push('>');
+var __val__ = jade.filters.markdown(desc)
+buf.push(null == __val__ ? "" : __val__);
+buf.push('\n    </div>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-links-row') + ' ' + ('row') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('span6') + ' ' + ('offset3') + ' ' + ('ug') + ' ' + ('graph-permalink') }));
+buf.push('>\n      <input');
+buf.push(attrs({ 'value':("" + (model.toPermalink()) + ""), 'readonly':("readonly"), "class": ('span6') }));
+buf.push('/>\n    </div>');
+if ( IS_DEV)
+{
+buf.push('<a');
+buf.push(attrs({ 'href':("#"), "class": ('span1') + ' ' + ('export-button') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-file') }));
+buf.push('></i> Export\n</a>');
+}
+buf.push('\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-notes-row') + ' ' + ('row') }));
+buf.push('>\n    <div');
+buf.push(attrs({ 'data-bind':('notes'), "class": ('span7') + ' ' + ('offset3') + ' ' + ('ug') + ' ' + ('graph-notes') }));
+buf.push('>');
+var __val__ = jade.filters.markdown(notes)
+buf.push(null == __val__ ? "" : __val__);
+buf.push('\n    </div>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/graph/graph-edit.jade.js b/lib/template/graph/graph-edit.jade.js
new file mode 100644 (file)
index 0000000..7b13171
--- /dev/null
@@ -0,0 +1,115 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ var graph_id = view.id || model.id || model.cid
+buf.push('\n<section');
+buf.push(attrs({ 'id':(graph_id), "class": ('graph-edit') + ' ' + ('graph') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('graph-name-row') + ' ' + ('graph-details') + ' ' + ('row-fluid') + ' ' + ('control-group') }));
+buf.push('>\n    <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_name1"), 'name':("name"), 'placeholder':('Graph Name'), 'value':(name), "class": ('span6') + ' ' + ('graph-name') }));
+buf.push('/>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-viewport-row') + ' ' + ('row-fluid') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('viewport') }));
+buf.push('>\n    </div>\n    <div');
+buf.push(attrs({ "class": ('graph-legend') }));
+buf.push('>\n    </div>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-settings-row') + ' ' + ('row-fluid') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('graph-settings') + ' ' + ('tabbable') }));
+buf.push('>\n      <nav');
+buf.push(attrs({ "class": ('graph-settings-nav') }));
+buf.push('>\n        <div');
+buf.push(attrs({ "class": ('graph-controls') + ' ' + ('pull-right') }));
+buf.push('>\n          <div');
+buf.push(attrs({ "class": ('btn-group') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('redraw-button') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-refresh') }));
+buf.push('></i> Redraw\n</a>\n          </div>\n          <div');
+buf.push(attrs({ "class": ('btn-group') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('load-button') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-download') }));
+buf.push('></i> Revert\n</a><a');
+buf.push(attrs({ 'href':("#"), "class": ('save-button') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-upload') }));
+buf.push('></i> Save\n</a><a');
+buf.push(attrs({ 'href':("#"), "class": ('done-button') + ' ' + ('btn-primary') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-ok-sign') + ' ' + ('icon-white') }));
+buf.push('></i> Done\n</a>\n          </div>\n        </div>\n        <ul');
+buf.push(attrs({ "class": ('nav') + ' ' + ('subnav') + ' ' + ('nav-pills') }));
+buf.push('>\n          <li>\n            <div');
+buf.push(attrs({ "class": ('graph-spinner') }));
+buf.push('>\n            </div>\n            <h3>Graph\n            </h3>\n          </li>\n          <li');
+buf.push(attrs({ "class": ('active') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#" + (graph_id) + "-tab-info"), 'data-toggle':("tab"), "class": ('graph-info-tab') }));
+buf.push('>Info</a>\n          </li>\n          <li><a');
+buf.push(attrs({ 'href':("#" + (graph_id) + "-tab-data"), 'data-toggle':("tab"), "class": ('graph-data-tab') }));
+buf.push('>Data</a>\n          </li>\n          <li><a');
+buf.push(attrs({ 'href':("#" + (graph_id) + "-tab-options"), 'data-toggle':("tab"), "class": ('graph-options-tab') }));
+buf.push('>Options</a>\n          </li>\n        </ul>\n        <div');
+buf.push(attrs({ "class": ('nav-shadow-shim') }));
+buf.push('>\n        </div>\n        <div');
+buf.push(attrs({ "class": ('nav-shadow') }));
+buf.push('>\n        </div>\n      </nav>\n      <div');
+buf.push(attrs({ "class": ('graph-tab-content') + ' ' + ('tab-content') }));
+buf.push('>\n        <div');
+buf.push(attrs({ 'id':("" + (graph_id) + "-tab-info"), "class": ('graph-info-pane') + ' ' + ('tab-pane') + ' ' + ('active') }));
+buf.push('>\n          <form');
+buf.push(attrs({ "class": ('graph-details') + ' ' + ('form-horizontal') }));
+buf.push('>\n            <input');
+buf.push(attrs({ 'type':("hidden"), 'id':("" + (graph_id) + "_name"), 'name':("name"), 'value':(name), "class": ('graph-name') }));
+buf.push('/>\n            <div');
+buf.push(attrs({ "class": ('row-fluid') }));
+buf.push('>\n              <div');
+buf.push(attrs({ "class": ('half') + ' ' + ('control-group') }));
+buf.push('>\n                <div');
+buf.push(attrs({ "class": ('control-group') }));
+buf.push('>\n                  <label');
+buf.push(attrs({ 'for':("" + (graph_id) + "_slug"), "class": ('slug') + ' ' + ('control-label') }));
+buf.push('>Slug\n                  </label>\n                  <div');
+buf.push(attrs({ "class": ('controls') }));
+buf.push('>\n                    <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_slug"), 'name':('slug'), 'placeholder':('graph_slug'), 'value':(slug), "class": ('span3') + ' ' + ('slug') }));
+buf.push('/>\n                    <p');
+buf.push(attrs({ "class": ('help-block') }));
+buf.push('>The slug uniquely identifies this graph and will be displayed in the URL once saved.\n                    </p>\n                  </div>\n                </div>\n                <div');
+buf.push(attrs({ "class": ('control-group') }));
+buf.push('>\n                  <label');
+buf.push(attrs({ 'for':("" + (graph_id) + "_width"), "class": ('width') + ' ' + ('control-label') }));
+buf.push('>Size\n                  </label>\n                  <div');
+buf.push(attrs({ "class": ('controls') }));
+buf.push('>\n                    <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_width"), 'name':('width'), 'value':(width), "class": ('span1') + ' ' + ('width') }));
+buf.push('/> &times; \n\n                    <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_height"), 'name':('height'), 'value':(height), "class": ('span1') + ' ' + ('height') }));
+buf.push('/>\n                    <p');
+buf.push(attrs({ "class": ('help-block') }));
+buf.push('>Choosing \'auto\' will size the graph to the viewport bounds.\n                    </p>\n                  </div>\n                </div>\n              </div>\n              <div');
+buf.push(attrs({ "class": ('half') + ' ' + ('control-group') }));
+buf.push('>\n                <label');
+buf.push(attrs({ 'for':("" + (graph_id) + "_desc"), "class": ('desc') + ' ' + ('control-label') }));
+buf.push('>Description\n                </label>\n                <div');
+buf.push(attrs({ "class": ('controls') }));
+buf.push('><textarea class="span3 desc" id="' + escape((interp = graph_id) == null ? '' : interp) + '_desc" name="desc" placeholder="Graph description.">' + escape((interp = desc) == null ? '' : interp) + '</textarea>\n\n                  <p');
+buf.push(attrs({ "class": ('help-block') }));
+buf.push('>A description of the graph.\n                  </p>\n                </div>\n              </div>\n            </div>\n          </form>\n        </div>\n        <div');
+buf.push(attrs({ 'id':("" + (graph_id) + "-tab-data"), 'data-subview':("DataView"), "class": ('graph-data-pane') + ' ' + ('tab-pane') }));
+buf.push('>\n        </div>\n        <div');
+buf.push(attrs({ 'id':("" + (graph_id) + "-tab-options"), 'data-subview':("ChartOptionScaffold"), "class": ('graph-options-pane') + ' ' + ('tab-pane') }));
+buf.push('>\n        </div>\n      </div>\n    </div>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/graph/graph-edit.jade.mod.js b/lib/template/graph/graph-edit.jade.mod.js
new file mode 100644 (file)
index 0000000..ce48362
--- /dev/null
@@ -0,0 +1,119 @@
+require.define('/node_modules/kraken/template/graph/graph-edit.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+ var graph_id = view.id || model.id || model.cid
+buf.push('\n<section');
+buf.push(attrs({ 'id':(graph_id), "class": ('graph-edit') + ' ' + ('graph') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('graph-name-row') + ' ' + ('graph-details') + ' ' + ('row-fluid') + ' ' + ('control-group') }));
+buf.push('>\n    <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_name1"), 'name':("name"), 'placeholder':('Graph Name'), 'value':(name), "class": ('span6') + ' ' + ('graph-name') }));
+buf.push('/>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-viewport-row') + ' ' + ('row-fluid') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('viewport') }));
+buf.push('>\n    </div>\n    <div');
+buf.push(attrs({ "class": ('graph-legend') }));
+buf.push('>\n    </div>\n  </div>\n  <div');
+buf.push(attrs({ "class": ('graph-settings-row') + ' ' + ('row-fluid') }));
+buf.push('>\n    <div');
+buf.push(attrs({ "class": ('graph-settings') + ' ' + ('tabbable') }));
+buf.push('>\n      <nav');
+buf.push(attrs({ "class": ('graph-settings-nav') }));
+buf.push('>\n        <div');
+buf.push(attrs({ "class": ('graph-controls') + ' ' + ('pull-right') }));
+buf.push('>\n          <div');
+buf.push(attrs({ "class": ('btn-group') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('redraw-button') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-refresh') }));
+buf.push('></i> Redraw\n</a>\n          </div>\n          <div');
+buf.push(attrs({ "class": ('btn-group') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#"), "class": ('load-button') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-download') }));
+buf.push('></i> Revert\n</a><a');
+buf.push(attrs({ 'href':("#"), "class": ('save-button') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-upload') }));
+buf.push('></i> Save\n</a><a');
+buf.push(attrs({ 'href':("#"), "class": ('done-button') + ' ' + ('btn-primary') + ' ' + ('btn') }));
+buf.push('><i');
+buf.push(attrs({ "class": ('icon-ok-sign') + ' ' + ('icon-white') }));
+buf.push('></i> Done\n</a>\n          </div>\n        </div>\n        <ul');
+buf.push(attrs({ "class": ('nav') + ' ' + ('subnav') + ' ' + ('nav-pills') }));
+buf.push('>\n          <li>\n            <div');
+buf.push(attrs({ "class": ('graph-spinner') }));
+buf.push('>\n            </div>\n            <h3>Graph\n            </h3>\n          </li>\n          <li');
+buf.push(attrs({ "class": ('active') }));
+buf.push('><a');
+buf.push(attrs({ 'href':("#" + (graph_id) + "-tab-info"), 'data-toggle':("tab"), "class": ('graph-info-tab') }));
+buf.push('>Info</a>\n          </li>\n          <li><a');
+buf.push(attrs({ 'href':("#" + (graph_id) + "-tab-data"), 'data-toggle':("tab"), "class": ('graph-data-tab') }));
+buf.push('>Data</a>\n          </li>\n          <li><a');
+buf.push(attrs({ 'href':("#" + (graph_id) + "-tab-options"), 'data-toggle':("tab"), "class": ('graph-options-tab') }));
+buf.push('>Options</a>\n          </li>\n        </ul>\n        <div');
+buf.push(attrs({ "class": ('nav-shadow-shim') }));
+buf.push('>\n        </div>\n        <div');
+buf.push(attrs({ "class": ('nav-shadow') }));
+buf.push('>\n        </div>\n      </nav>\n      <div');
+buf.push(attrs({ "class": ('graph-tab-content') + ' ' + ('tab-content') }));
+buf.push('>\n        <div');
+buf.push(attrs({ 'id':("" + (graph_id) + "-tab-info"), "class": ('graph-info-pane') + ' ' + ('tab-pane') + ' ' + ('active') }));
+buf.push('>\n          <form');
+buf.push(attrs({ "class": ('graph-details') + ' ' + ('form-horizontal') }));
+buf.push('>\n            <input');
+buf.push(attrs({ 'type':("hidden"), 'id':("" + (graph_id) + "_name"), 'name':("name"), 'value':(name), "class": ('graph-name') }));
+buf.push('/>\n            <div');
+buf.push(attrs({ "class": ('row-fluid') }));
+buf.push('>\n              <div');
+buf.push(attrs({ "class": ('half') + ' ' + ('control-group') }));
+buf.push('>\n                <div');
+buf.push(attrs({ "class": ('control-group') }));
+buf.push('>\n                  <label');
+buf.push(attrs({ 'for':("" + (graph_id) + "_slug"), "class": ('slug') + ' ' + ('control-label') }));
+buf.push('>Slug\n                  </label>\n                  <div');
+buf.push(attrs({ "class": ('controls') }));
+buf.push('>\n                    <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_slug"), 'name':('slug'), 'placeholder':('graph_slug'), 'value':(slug), "class": ('span3') + ' ' + ('slug') }));
+buf.push('/>\n                    <p');
+buf.push(attrs({ "class": ('help-block') }));
+buf.push('>The slug uniquely identifies this graph and will be displayed in the URL once saved.\n                    </p>\n                  </div>\n                </div>\n                <div');
+buf.push(attrs({ "class": ('control-group') }));
+buf.push('>\n                  <label');
+buf.push(attrs({ 'for':("" + (graph_id) + "_width"), "class": ('width') + ' ' + ('control-label') }));
+buf.push('>Size\n                  </label>\n                  <div');
+buf.push(attrs({ "class": ('controls') }));
+buf.push('>\n                    <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_width"), 'name':('width'), 'value':(width), "class": ('span1') + ' ' + ('width') }));
+buf.push('/> &times; \n\n                    <input');
+buf.push(attrs({ 'type':('text'), 'id':("" + (graph_id) + "_height"), 'name':('height'), 'value':(height), "class": ('span1') + ' ' + ('height') }));
+buf.push('/>\n                    <p');
+buf.push(attrs({ "class": ('help-block') }));
+buf.push('>Choosing \'auto\' will size the graph to the viewport bounds.\n                    </p>\n                  </div>\n                </div>\n              </div>\n              <div');
+buf.push(attrs({ "class": ('half') + ' ' + ('control-group') }));
+buf.push('>\n                <label');
+buf.push(attrs({ 'for':("" + (graph_id) + "_desc"), "class": ('desc') + ' ' + ('control-label') }));
+buf.push('>Description\n                </label>\n                <div');
+buf.push(attrs({ "class": ('controls') }));
+buf.push('><textarea class="span3 desc" id="' + escape((interp = graph_id) == null ? '' : interp) + '_desc" name="desc" placeholder="Graph description.">' + escape((interp = desc) == null ? '' : interp) + '</textarea>\n\n                  <p');
+buf.push(attrs({ "class": ('help-block') }));
+buf.push('>A description of the graph.\n                  </p>\n                </div>\n              </div>\n            </div>\n          </form>\n        </div>\n        <div');
+buf.push(attrs({ 'id':("" + (graph_id) + "-tab-data"), 'data-subview':("DataView"), "class": ('graph-data-pane') + ' ' + ('tab-pane') }));
+buf.push('>\n        </div>\n        <div');
+buf.push(attrs({ 'id':("" + (graph_id) + "-tab-options"), 'data-subview':("ChartOptionScaffold"), "class": ('graph-options-pane') + ' ' + ('tab-pane') }));
+buf.push('>\n        </div>\n      </div>\n    </div>\n  </div>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/template/graph/graph-list.jade.js b/lib/template/graph/graph-list.jade.js
new file mode 100644 (file)
index 0000000..986a0b7
--- /dev/null
@@ -0,0 +1,38 @@
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ 'id':('graph-list') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('page-header') }));
+buf.push('>\n    <h1>Saved Graphs\n    </h1>\n  </div>\n  <ul>');
+// iterate collection.models
+(function(){
+  if ('number' == typeof collection.models.length) {
+    for (var $index = 0, $$l = collection.models.length; $index < $$l; $index++) {
+      var graph = collection.models[$index];
+
+buf.push('\n    <li><a');
+buf.push(attrs({ 'href':("" + (graph.toLink()) + "") }));
+buf.push('>' + escape((interp = graph.get('name')) == null ? '' : interp) + '</a>\n    </li>');
+    }
+  } else {
+    for (var $index in collection.models) {
+      var graph = collection.models[$index];
+
+buf.push('\n    <li><a');
+buf.push(attrs({ 'href':("" + (graph.toLink()) + "") }));
+buf.push('>' + escape((interp = graph.get('name')) == null ? '' : interp) + '</a>\n    </li>');
+   }
+  }
+}).call(this);
+
+buf.push('\n  </ul>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
\ No newline at end of file
diff --git a/lib/template/graph/graph-list.jade.mod.js b/lib/template/graph/graph-list.jade.mod.js
new file mode 100644 (file)
index 0000000..9d93f7d
--- /dev/null
@@ -0,0 +1,42 @@
+require.define('/node_modules/kraken/template/graph/graph-list.jade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var template = function (locals, attrs, escape, rethrow) {
+var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;
+var buf = [];
+with (locals || {}) {
+var interp;
+buf.push('\n<section');
+buf.push(attrs({ 'id':('graph-list') }));
+buf.push('>\n  <div');
+buf.push(attrs({ "class": ('page-header') }));
+buf.push('>\n    <h1>Saved Graphs\n    </h1>\n  </div>\n  <ul>');
+// iterate collection.models
+(function(){
+  if ('number' == typeof collection.models.length) {
+    for (var $index = 0, $$l = collection.models.length; $index < $$l; $index++) {
+      var graph = collection.models[$index];
+
+buf.push('\n    <li><a');
+buf.push(attrs({ 'href':("" + (graph.toLink()) + "") }));
+buf.push('>' + escape((interp = graph.get('name')) == null ? '' : interp) + '</a>\n    </li>');
+    }
+  } else {
+    for (var $index in collection.models) {
+      var graph = collection.models[$index];
+
+buf.push('\n    <li><a');
+buf.push(attrs({ 'href':("" + (graph.toLink()) + "") }));
+buf.push('>' + escape((interp = graph.get('name')) == null ? '' : interp) + '</a>\n    </li>');
+   }
+  }
+}).call(this);
+
+buf.push('\n  </ul>\n</section>');
+}
+return buf.join("");
+};
+if (typeof module != 'undefined') {
+    module.exports = exports = template;
+}
+
+});
diff --git a/lib/util/backbone.js b/lib/util/backbone.js
new file mode 100644 (file)
index 0000000..83f6ed6
--- /dev/null
@@ -0,0 +1,130 @@
+var Backbone, that, methodize, Cls, _, _bb_events, _backbone, _methodized, _i, _ref, _len, __slice = [].slice;
+_ = require('underscore');
+if (typeof window != 'undefined' && window !== null) {
+  window._ = _;
+}
+Backbone = require('backbone');
+if (typeof window != 'undefined' && window !== null) {
+  window.Backbone = Backbone;
+}
+if (that = (typeof window != 'undefined' && window !== null) && (window.jQuery || window.Zepto || window.ender)) {
+  Backbone.setDomLibrary(that);
+}
+_bb_events = {
+  /**
+   * Registers an event listener on the given event(s) to be fired only once.
+   * 
+   * @param {String} events Space delimited list of event names.
+   * @param {Function} callback Event listener function.
+   * @param {Object} [context=this] Object to be supplied as the context for the listener.
+   * @returns {this}
+   */
+  once: function(events, callback, context){
+    var fn, _this = this;
+    fn = function(){
+      _this.off(events, arguments.callee, _this);
+      return callback.apply(context || _this, arguments);
+    };
+    this.on(events, fn, this);
+    return this;
+  }
+  /**
+   * Compatibility with Node's `EventEmitter`.
+   */,
+  emit: Backbone.Events.trigger
+};
+/**
+ * @namespace Meta-utilities for working with Backbone classes.
+ */
+_backbone = {
+  /**
+   * @returns {Array<Class>} The list of all superclasses for this class or object.
+   */
+  getSuperClasses: (function(){
+    function getSuperClasses(Cls){
+      var that, superclass, _ref;
+      if (!Cls) {
+        return [];
+      }
+      if (that = Cls.__superclass__) {
+        superclass = that;
+      } else {
+        if (typeof Cls !== 'function') {
+          Cls = Cls.constructor;
+        }
+        if (that = (_ref = Cls.__super__) != null ? _ref.constructor : void 8) {
+          superclass = that;
+        } else if (Cls.prototype.constructor !== Cls) {
+          superclass;
+        }
+      }
+      if (superclass) {
+        return [superclass].concat(getSuperClasses(superclass));
+      } else {
+        return [];
+      }
+    }
+    return getSuperClasses;
+  }())
+  /**
+   * Looks up an attribute on the prototype of each class in the class
+   * hierarchy.
+   * @returns {Array}
+   */,
+  pluckSuper: function(obj, prop){
+    if (!obj) {
+      return [];
+    }
+    return _(_backbone.getSuperClasses(obj)).chain().pluck('prototype').pluck(prop).value();
+  }
+  /**
+   * As `.pluckSuper()` but includes value of `prop` on passed `obj`.
+   * @returns {Array}
+   */,
+  pluckSuperAndSelf: function(obj, prop){
+    if (!obj) {
+      return [];
+    }
+    return [obj[prop]].concat(_backbone.pluckSuper(obj, prop));
+  }
+};
+__import(exports, _backbone);
+/**
+ * Decorates a function so that its receiver (`this`) is always added as the
+ * first argument, followed by the call arguments.
+ * @returns {Function}
+ */
+methodize = exports.methodize = function(fn){
+  var m, g, that;
+  m = fn.__methodized__;
+  if (m) {
+    return m;
+  }
+  g = fn.__genericized__;
+  if (that = g != null ? g.__wraps__ : void 8) {
+    return that;
+  }
+  m = fn.__methodized__ = function(){
+    var args;
+    args = __slice.call(arguments);
+    args.unshift(this);
+    return fn.apply(this, args);
+  };
+  m.__wraps__ = fn;
+  return m;
+};
+_methodized = exports._methodized = _.reduce(_backbone, function(o, v, k){
+  o[k] = typeof v === 'function' ? methodize(v) : v;
+  return o;
+}, {});
+_.extend(Backbone.Events, _bb_events);
+for (_i = 0, _len = (_ref = [Backbone['Model'], Backbone['Collection'], Backbone['View']]).length; _i < _len; ++_i) {
+  Cls = _ref[_i];
+  __import(__import(__import(Cls, _methodized), _bb_events), Backbone.Events);
+  __import(__import(Cls.prototype, _methodized), _bb_events);
+}
+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/util/backbone.mod.js b/lib/util/backbone.mod.js
new file mode 100644 (file)
index 0000000..1ac7157
--- /dev/null
@@ -0,0 +1,134 @@
+require.define('/node_modules/kraken/util/backbone.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var Backbone, that, methodize, Cls, _, _bb_events, _backbone, _methodized, _i, _ref, _len, __slice = [].slice;
+_ = require('underscore');
+if (typeof window != 'undefined' && window !== null) {
+  window._ = _;
+}
+Backbone = require('backbone');
+if (typeof window != 'undefined' && window !== null) {
+  window.Backbone = Backbone;
+}
+if (that = (typeof window != 'undefined' && window !== null) && (window.jQuery || window.Zepto || window.ender)) {
+  Backbone.setDomLibrary(that);
+}
+_bb_events = {
+  /**
+   * Registers an event listener on the given event(s) to be fired only once.
+   * 
+   * @param {String} events Space delimited list of event names.
+   * @param {Function} callback Event listener function.
+   * @param {Object} [context=this] Object to be supplied as the context for the listener.
+   * @returns {this}
+   */
+  once: function(events, callback, context){
+    var fn, _this = this;
+    fn = function(){
+      _this.off(events, arguments.callee, _this);
+      return callback.apply(context || _this, arguments);
+    };
+    this.on(events, fn, this);
+    return this;
+  }
+  /**
+   * Compatibility with Node's `EventEmitter`.
+   */,
+  emit: Backbone.Events.trigger
+};
+/**
+ * @namespace Meta-utilities for working with Backbone classes.
+ */
+_backbone = {
+  /**
+   * @returns {Array<Class>} The list of all superclasses for this class or object.
+   */
+  getSuperClasses: (function(){
+    function getSuperClasses(Cls){
+      var that, superclass, _ref;
+      if (!Cls) {
+        return [];
+      }
+      if (that = Cls.__superclass__) {
+        superclass = that;
+      } else {
+        if (typeof Cls !== 'function') {
+          Cls = Cls.constructor;
+        }
+        if (that = (_ref = Cls.__super__) != null ? _ref.constructor : void 8) {
+          superclass = that;
+        } else if (Cls.prototype.constructor !== Cls) {
+          superclass;
+        }
+      }
+      if (superclass) {
+        return [superclass].concat(getSuperClasses(superclass));
+      } else {
+        return [];
+      }
+    }
+    return getSuperClasses;
+  }())
+  /**
+   * Looks up an attribute on the prototype of each class in the class
+   * hierarchy.
+   * @returns {Array}
+   */,
+  pluckSuper: function(obj, prop){
+    if (!obj) {
+      return [];
+    }
+    return _(_backbone.getSuperClasses(obj)).chain().pluck('prototype').pluck(prop).value();
+  }
+  /**
+   * As `.pluckSuper()` but includes value of `prop` on passed `obj`.
+   * @returns {Array}
+   */,
+  pluckSuperAndSelf: function(obj, prop){
+    if (!obj) {
+      return [];
+    }
+    return [obj[prop]].concat(_backbone.pluckSuper(obj, prop));
+  }
+};
+__import(exports, _backbone);
+/**
+ * Decorates a function so that its receiver (`this`) is always added as the
+ * first argument, followed by the call arguments.
+ * @returns {Function}
+ */
+methodize = exports.methodize = function(fn){
+  var m, g, that;
+  m = fn.__methodized__;
+  if (m) {
+    return m;
+  }
+  g = fn.__genericized__;
+  if (that = g != null ? g.__wraps__ : void 8) {
+    return that;
+  }
+  m = fn.__methodized__ = function(){
+    var args;
+    args = __slice.call(arguments);
+    args.unshift(this);
+    return fn.apply(this, args);
+  };
+  m.__wraps__ = fn;
+  return m;
+};
+_methodized = exports._methodized = _.reduce(_backbone, function(o, v, k){
+  o[k] = typeof v === 'function' ? methodize(v) : v;
+  return o;
+}, {});
+_.extend(Backbone.Events, _bb_events);
+for (_i = 0, _len = (_ref = [Backbone['Model'], Backbone['Collection'], Backbone['View']]).length; _i < _len; ++_i) {
+  Cls = _ref[_i];
+  __import(__import(__import(Cls, _methodized), _bb_events), Backbone.Events);
+  __import(__import(Cls.prototype, _methodized), _bb_events);
+}
+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/util/cascade.js b/lib/util/cascade.js
new file mode 100644 (file)
index 0000000..ab6a5d4
--- /dev/null
@@ -0,0 +1,417 @@
+var hasOwn, MISSING, TOMBSTONE, Cascade, ALIASES, dest, src, exports, _, __slice = [].slice;
+_ = require('kraken/util/underscore');
+hasOwn = {}.hasOwnProperty;
+/**
+ * Sentinel for missing values.
+ */
+MISSING = void 8;
+/**
+ * Tombstone for deleted, non-passthrough keys.
+ */
+TOMBSTONE = {};
+/**
+ * @class A mapping of key-value pairs supporting lookup fallback across multiple objects.
+ */
+Cascade = (function(){
+  /**
+   * Sentinel tombstone for deleted, non-passthrough keys.
+   * @type TOMBSTONE
+   * @readonly
+   */
+  Cascade.displayName = 'Cascade';
+  var prototype = Cascade.prototype, constructor = Cascade;
+  Cascade.TOMBSTONE = TOMBSTONE;
+  /**
+   * Map holding the object's KV-pairs; always the second element of the
+   * cascade lookup.
+   * @type Object
+   * @private
+   */
+  prototype._data = null;
+  /**
+   * Map of tombstones, marking intentionally unset keys in the object's
+   * KV-pairs; always the first element of the cascade lookup.
+   * @type Object<String, TOMBSTONE>
+   * @private
+   */
+  prototype._tombstones = null;
+  /**
+   * List of objects for lookups.
+   * @type Array
+   * @private
+   */
+  prototype._lookups = null;
+  /**
+   * @constructor
+   */;
+  function Cascade(data, lookups, tombstones){
+    data == null && (data = {});
+    lookups == null && (lookups = []);
+    tombstones == null && (tombstones = {});
+    this._data = data;
+    this._tombstones = tombstones;
+    this._lookups = [this._data].concat(lookups);
+  }
+  /**
+   * @returns {Cascade} A copy of the data and lookup chain.
+   */
+  prototype.clone = function(){
+    return new Cascade(__import({}, this._data), this._lookups.slice(), __import({}, this._tombstones));
+  };
+  prototype.getData = function(){
+    return this._data;
+  };
+  prototype.setData = function(data){
+    this._data = this._lookups[0] = data;
+    return this;
+  };
+  prototype.getTombstones = function(){
+    return this._tombstones;
+  };
+  /**
+   * @returns {Number} Number of lookup dictionaries.
+   */
+  prototype.size = function(){
+    return this._lookups.length - 1;
+  };
+  /**
+   * @returns {Array<Object>} The array of lookup dictionaries.
+   */
+  prototype.getLookups = function(){
+    return this._lookups;
+  };
+  /**
+   * @returns {Array<Object>} The array of lookup dictionaries.
+   */
+  prototype.getLookups = function(){
+    return this._lookups;
+  };
+  /**
+   * Adds a new lookup dictionary to the chain.
+   * @returns {this}
+   */
+  prototype.addLookup = function(dict){
+    if (dict == null) {
+      return this;
+    }
+    if (!_.isObject(dict)) {
+      throw new Error("Lookup dictionary must be an object! dict=" + dict);
+    }
+    this._lookups.push(dict);
+    return this;
+  };
+  /**
+   * Removes a lookup dictionary from the chain (but will not remove the data object).
+   * @returns {this}
+   */
+  prototype.removeLookup = function(dict){
+    if (dict && dict !== this._data) {
+      _.remove(this._lookups, dict);
+    }
+    return this;
+  };
+  /**
+   * Pops the last dictionary off the lookup chain and returns it.
+   * @returns {*} The last dictionary, or `undefined` if there are no additional lookups.
+   */
+  prototype.popLookup = function(){
+    if (this.size() <= 1) {
+      return;
+    }
+    return this._lookups.pop();
+  };
+  /**
+   * Shifts the first additional lookup dictionary off the chain and returns it.
+   * @returns {*} The first dictionary, or `undefined` if there are no additional lookups.
+   */
+  prototype.shiftLookup = function(){
+    if (this.size() <= 1) {
+      return;
+    }
+    return this._lookups.splice(1, 1)[0];
+  };
+  /**
+   * Adds a lookup dictionary to the front of the chain, just after the Cascade's own data
+   * object.
+   * @returns {this}
+   */
+  prototype.unshiftLookup = function(dict){
+    if (dict == null) {
+      return this;
+    }
+    if (!_.isObject(dict)) {
+      throw new Error("Lookup dictionary must be an object! dict=" + dict);
+    }
+    this._lookups.splice(1, 0, dict);
+    return this;
+  };
+  /**
+   * @returns {Boolean} Whether there is a tombstone set for `key`.
+   */
+  prototype.hasTombstone = function(key){
+    var o, part, _i, _ref, _len;
+    o = this._tombstones;
+    for (_i = 0, _len = (_ref = key.split('.')).length; _i < _len; ++_i) {
+      part = _ref[_i];
+      o = o[part];
+      if (o === TOMBSTONE) {
+        return true;
+      }
+      if (!o) {
+        return false;
+      }
+    }
+    return false;
+  };
+  /**
+   * @returns {Boolean} Whether `key` belongs to this object (not inherited
+   *  from the cascade).
+   */
+  prototype.isOwnProperty = function(key){
+    var meta;
+    if (this.hasTombstone(key)) {
+      return true;
+    }
+    meta = _.getNestedMeta(this._data, key);
+    return (meta != null ? meta.obj : void 8) && hasOwn.call(meta.obj, key);
+  };
+  /**
+   * @returns {Boolean} Whether `key` belongs to this object (not inherited
+   *  from the cascade) and is defined.
+   */
+  prototype.isOwnValue = function(key){
+    return !this.hasTombstone(key) && this.isOwnProperty(key) && _.getNested(this._data, key, MISSING) !== MISSING;
+  };
+  /**
+   * @returns {Boolean} Whether the value at `key` is the same as that
+   *  inherited by from the cascade.
+   */
+  prototype.isInheritedValue = function(key, strict){
+    var val, cVal;
+    strict == null && (strict = false);
+    if (this.hasTombstone(key)) {
+      return false;
+    }
+    val = this.get(key);
+    cVal = this._getInCascade(key, MISSING, 2);
+    if (strict) {
+      return val === cVal;
+    } else {
+      return _.isEqual(val, cVal);
+    }
+  };
+  /**
+   * @returns {Boolean} Whether the value at `key` is different from that
+   *  inherited by from the cascade.
+   */
+  prototype.isModifiedValue = function(key, strict){
+    strict == null && (strict = false);
+    return !this.isInheritedValue(key, strict);
+  };
+  /**
+   * @private
+   * @param {String} key Key to look up.
+   * @param {*} [def=undefined] Value to return if lookup fails.
+   * @param {Number} [idx=0] Index into lookup list to begin search.
+   * @returns {*} First value for `key` found in the lookup chain starting at `idx`,
+   *  and `def` otherwise.
+   */
+  prototype._getInCascade = function(key, def, idx){
+    var lookups, data, val, _i, _len;
+    idx == null && (idx = 0);
+    if (this.hasTombstone(key)) {
+      return def;
+    }
+    lookups = idx
+      ? this._lookups.slice(idx)
+      : this._lookups;
+    for (_i = 0, _len = lookups.length; _i < _len; ++_i) {
+      data = lookups[_i];
+      val = _.getNested(data, key, MISSING, {
+        tombstone: TOMBSTONE
+      });
+      if (val === TOMBSTONE) {
+        return def;
+      }
+      if (val !== MISSING) {
+        return val;
+      }
+    }
+    return def;
+  };
+  /**
+   * @returns {Boolean} Whether there is a value at the given key.
+   */
+  prototype.has = function(key){
+    return this.get(key, MISSING) !== MISSING;
+  };
+  /**
+   * @param {String} key Key to look up.
+   * @param {*} [def=undefined] Value to return if lookup fails.
+   * @returns {*} First value for `key` found in the lookup chain,
+   *  and `def` otherwise.
+   */
+  prototype.get = function(key, def){
+    return this._getInCascade(key, def);
+  };
+  /**
+   * Sets a key to a value, accepting nested keys and creating intermediary objects as necessary.
+   * @public
+   * @name set
+   * @param {String} key Key to set.
+   * @param {*} value Non-`undefined` value to set.
+   * @returns {this}
+   */
+  /**
+   * @public
+   * @name set
+   * @param {Object} values Map of pairs to set. No value may be `undefined`.
+   * @returns {this}
+   */
+  prototype.set = function(values){
+    var key, val, _ref;
+    if (arguments.length > 1 && typeof values === 'string') {
+      key = arguments[0], val = arguments[1];
+      if (!key || val === void 8) {
+        throw new Error("Value and key cannot be undefined!");
+      }
+      values = (_ref = {}, _ref[key + ""] = val, _ref);
+    }
+    for (key in values) {
+      val = values[key];
+      _.unsetNested(this._tombstones, key, {
+        ensure: true
+      });
+      _.setNested(this._data, key, val, {
+        ensure: true
+      });
+    }
+    return this;
+  };
+  /**
+   * Delete the given key from this object's data dictionary and set a tombstone
+   * which ensures that future lookups do not cascade and thus see the key as
+   * `undefined`.
+   * 
+   * If the key is missing from the data dictionary the delete does not cascade,
+   * but the tombstone is still set.
+   * 
+   * @param {String} key Key to unset.
+   * @returns {undefined|*} If found, returns the old value, and otherwise `undefined`.
+   */
+  prototype.unset = function(key){
+    var old;
+    old = this.get(key);
+    _.unsetNested(this._data, key);
+    _.setNested(this._tombstones, key, TOMBSTONE, {
+      ensure: true
+    });
+    return old;
+  };
+  /**
+   * Unsets the key in the data dictionary, but ensures future lookups also
+   * see the key as `undefined`, as opposed.
+   * 
+   * @param {String} key Key to unset.
+   * @returns {this} 
+   */
+  prototype.inherit = function(key){
+    _.unsetNested(this._tombstones, key, {
+      ensure: true
+    });
+    return _.unsetNested(this._data, key);
+  };
+  prototype.extend = function(){
+    var o, _i, _len;
+    for (_i = 0, _len = arguments.length; _i < _len; ++_i) {
+      o = arguments[_i];
+      this.set(o);
+    }
+    return this;
+  };
+  /**
+   * Recursively collapses the Cascade to a plain object by recursively merging the
+   * lookups (in reverse order) into the data.
+   * @returns {Object}
+   */
+  prototype.collapse = function(){
+    var o, k;
+    o = _.merge.apply(_, [{}].concat(__slice.call(this._lookups.slice(1).reverse())));
+    for (k in this._tombstones) {
+      delete o[k];
+    }
+    return _.merge(o, this._data);
+  };
+  /**
+   * Returns a plain object for JSON serialization via {@link Cascade#collapse()}.
+   * The name of this method is a bit confusing, as it doesn't actually return a 
+   * JSON string -- but I'm afraid that it's the way that the JavaScript API for 
+   * `JSON.stringify()` works.
+   * 
+   * @see https://developer.mozilla.org/en/JSON#toJSON()_method
+   * @return {Object} Plain object for JSON serialization.
+   */
+  prototype.toJSON = function(){
+    return this.collapse();
+  };
+  prototype.keys = function(){
+    return _.flatten(_.map(this._lookups, function(it){
+      return _.keys(it);
+    }));
+  };
+  prototype.values = function(){
+    return _.flatten(_.map(this._lookups, function(it){
+      return _.values(it);
+    }));
+  };
+  prototype.reduce = function(fn, acc, context){
+    context == null && (context = this);
+    return _.reduce(this._lookups, fn, acc, context);
+  };
+  prototype.map = function(fn, context){
+    context == null && (context = this);
+    return _.map(this._lookups, fn, context);
+  };
+  prototype.filter = function(fn, context){
+    context == null && (context = this);
+    return _.filter(this._lookups, fn, context);
+  };
+  prototype.each = function(fn, context){
+    context == null && (context = this);
+    _.each(this._lookups, fn, context);
+    return this;
+  };
+  prototype.invoke = function(name){
+    var args;
+    args = __slice.call(arguments, 1);
+    return _.invoke.apply(_, [this._lookups, name].concat(__slice.call(args)));
+  };
+  prototype.pluck = function(attr){
+    return _.pluck(this._lookups, attr);
+  };
+  prototype.find = function(fn, context){
+    context == null && (context = this);
+    return _.find(this._lookups, fn, context);
+  };
+  prototype.toString = function(){
+    var Cls;
+    Cls = this.constructor;
+    return (Cls.displayName || Cls.name) + "()";
+  };
+  return Cascade;
+}());
+ALIASES = {
+  setTombstone: 'unset',
+  toObject: 'collapse',
+  forEach: 'each'
+};
+for (dest in ALIASES) {
+  src = ALIASES[dest];
+  Cascade.prototype[dest] = Cascade.prototype[src];
+}
+module.exports = exports = Cascade;
+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/util/cascade.mod.js b/lib/util/cascade.mod.js
new file mode 100644 (file)
index 0000000..356c618
--- /dev/null
@@ -0,0 +1,421 @@
+require.define('/node_modules/kraken/util/cascade.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var hasOwn, MISSING, TOMBSTONE, Cascade, ALIASES, dest, src, exports, _, __slice = [].slice;
+_ = require('kraken/util/underscore');
+hasOwn = {}.hasOwnProperty;
+/**
+ * Sentinel for missing values.
+ */
+MISSING = void 8;
+/**
+ * Tombstone for deleted, non-passthrough keys.
+ */
+TOMBSTONE = {};
+/**
+ * @class A mapping of key-value pairs supporting lookup fallback across multiple objects.
+ */
+Cascade = (function(){
+  /**
+   * Sentinel tombstone for deleted, non-passthrough keys.
+   * @type TOMBSTONE
+   * @readonly
+   */
+  Cascade.displayName = 'Cascade';
+  var prototype = Cascade.prototype, constructor = Cascade;
+  Cascade.TOMBSTONE = TOMBSTONE;
+  /**
+   * Map holding the object's KV-pairs; always the second element of the
+   * cascade lookup.
+   * @type Object
+   * @private
+   */
+  prototype._data = null;
+  /**
+   * Map of tombstones, marking intentionally unset keys in the object's
+   * KV-pairs; always the first element of the cascade lookup.
+   * @type Object<String, TOMBSTONE>
+   * @private
+   */
+  prototype._tombstones = null;
+  /**
+   * List of objects for lookups.
+   * @type Array
+   * @private
+   */
+  prototype._lookups = null;
+  /**
+   * @constructor
+   */;
+  function Cascade(data, lookups, tombstones){
+    data == null && (data = {});
+    lookups == null && (lookups = []);
+    tombstones == null && (tombstones = {});
+    this._data = data;
+    this._tombstones = tombstones;
+    this._lookups = [this._data].concat(lookups);
+  }
+  /**
+   * @returns {Cascade} A copy of the data and lookup chain.
+   */
+  prototype.clone = function(){
+    return new Cascade(__import({}, this._data), this._lookups.slice(), __import({}, this._tombstones));
+  };
+  prototype.getData = function(){
+    return this._data;
+  };
+  prototype.setData = function(data){
+    this._data = this._lookups[0] = data;
+    return this;
+  };
+  prototype.getTombstones = function(){
+    return this._tombstones;
+  };
+  /**
+   * @returns {Number} Number of lookup dictionaries.
+   */
+  prototype.size = function(){
+    return this._lookups.length - 1;
+  };
+  /**
+   * @returns {Array<Object>} The array of lookup dictionaries.
+   */
+  prototype.getLookups = function(){
+    return this._lookups;
+  };
+  /**
+   * @returns {Array<Object>} The array of lookup dictionaries.
+   */
+  prototype.getLookups = function(){
+    return this._lookups;
+  };
+  /**
+   * Adds a new lookup dictionary to the chain.
+   * @returns {this}
+   */
+  prototype.addLookup = function(dict){
+    if (dict == null) {
+      return this;
+    }
+    if (!_.isObject(dict)) {
+      throw new Error("Lookup dictionary must be an object! dict=" + dict);
+    }
+    this._lookups.push(dict);
+    return this;
+  };
+  /**
+   * Removes a lookup dictionary from the chain (but will not remove the data object).
+   * @returns {this}
+   */
+  prototype.removeLookup = function(dict){
+    if (dict && dict !== this._data) {
+      _.remove(this._lookups, dict);
+    }
+    return this;
+  };
+  /**
+   * Pops the last dictionary off the lookup chain and returns it.
+   * @returns {*} The last dictionary, or `undefined` if there are no additional lookups.
+   */
+  prototype.popLookup = function(){
+    if (this.size() <= 1) {
+      return;
+    }
+    return this._lookups.pop();
+  };
+  /**
+   * Shifts the first additional lookup dictionary off the chain and returns it.
+   * @returns {*} The first dictionary, or `undefined` if there are no additional lookups.
+   */
+  prototype.shiftLookup = function(){
+    if (this.size() <= 1) {
+      return;
+    }
+    return this._lookups.splice(1, 1)[0];
+  };
+  /**
+   * Adds a lookup dictionary to the front of the chain, just after the Cascade's own data
+   * object.
+   * @returns {this}
+   */
+  prototype.unshiftLookup = function(dict){
+    if (dict == null) {
+      return this;
+    }
+    if (!_.isObject(dict)) {
+      throw new Error("Lookup dictionary must be an object! dict=" + dict);
+    }
+    this._lookups.splice(1, 0, dict);
+    return this;
+  };
+  /**
+   * @returns {Boolean} Whether there is a tombstone set for `key`.
+   */
+  prototype.hasTombstone = function(key){
+    var o, part, _i, _ref, _len;
+    o = this._tombstones;
+    for (_i = 0, _len = (_ref = key.split('.')).length; _i < _len; ++_i) {
+      part = _ref[_i];
+      o = o[part];
+      if (o === TOMBSTONE) {
+        return true;
+      }
+      if (!o) {
+        return false;
+      }
+    }
+    return false;
+  };
+  /**
+   * @returns {Boolean} Whether `key` belongs to this object (not inherited
+   *  from the cascade).
+   */
+  prototype.isOwnProperty = function(key){
+    var meta;
+    if (this.hasTombstone(key)) {
+      return true;
+    }
+    meta = _.getNestedMeta(this._data, key);
+    return (meta != null ? meta.obj : void 8) && hasOwn.call(meta.obj, key);
+  };
+  /**
+   * @returns {Boolean} Whether `key` belongs to this object (not inherited
+   *  from the cascade) and is defined.
+   */
+  prototype.isOwnValue = function(key){
+    return !this.hasTombstone(key) && this.isOwnProperty(key) && _.getNested(this._data, key, MISSING) !== MISSING;
+  };
+  /**
+   * @returns {Boolean} Whether the value at `key` is the same as that
+   *  inherited by from the cascade.
+   */
+  prototype.isInheritedValue = function(key, strict){
+    var val, cVal;
+    strict == null && (strict = false);
+    if (this.hasTombstone(key)) {
+      return false;
+    }
+    val = this.get(key);
+    cVal = this._getInCascade(key, MISSING, 2);
+    if (strict) {
+      return val === cVal;
+    } else {
+      return _.isEqual(val, cVal);
+    }
+  };
+  /**
+   * @returns {Boolean} Whether the value at `key` is different from that
+   *  inherited by from the cascade.
+   */
+  prototype.isModifiedValue = function(key, strict){
+    strict == null && (strict = false);
+    return !this.isInheritedValue(key, strict);
+  };
+  /**
+   * @private
+   * @param {String} key Key to look up.
+   * @param {*} [def=undefined] Value to return if lookup fails.
+   * @param {Number} [idx=0] Index into lookup list to begin search.
+   * @returns {*} First value for `key` found in the lookup chain starting at `idx`,
+   *  and `def` otherwise.
+   */
+  prototype._getInCascade = function(key, def, idx){
+    var lookups, data, val, _i, _len;
+    idx == null && (idx = 0);
+    if (this.hasTombstone(key)) {
+      return def;
+    }
+    lookups = idx
+      ? this._lookups.slice(idx)
+      : this._lookups;
+    for (_i = 0, _len = lookups.length; _i < _len; ++_i) {
+      data = lookups[_i];
+      val = _.getNested(data, key, MISSING, {
+        tombstone: TOMBSTONE
+      });
+      if (val === TOMBSTONE) {
+        return def;
+      }
+      if (val !== MISSING) {
+        return val;
+      }
+    }
+    return def;
+  };
+  /**
+   * @returns {Boolean} Whether there is a value at the given key.
+   */
+  prototype.has = function(key){
+    return this.get(key, MISSING) !== MISSING;
+  };
+  /**
+   * @param {String} key Key to look up.
+   * @param {*} [def=undefined] Value to return if lookup fails.
+   * @returns {*} First value for `key` found in the lookup chain,
+   *  and `def` otherwise.
+   */
+  prototype.get = function(key, def){
+    return this._getInCascade(key, def);
+  };
+  /**
+   * Sets a key to a value, accepting nested keys and creating intermediary objects as necessary.
+   * @public
+   * @name set
+   * @param {String} key Key to set.
+   * @param {*} value Non-`undefined` value to set.
+   * @returns {this}
+   */
+  /**
+   * @public
+   * @name set
+   * @param {Object} values Map of pairs to set. No value may be `undefined`.
+   * @returns {this}
+   */
+  prototype.set = function(values){
+    var key, val, _ref;
+    if (arguments.length > 1 && typeof values === 'string') {
+      key = arguments[0], val = arguments[1];
+      if (!key || val === void 8) {
+        throw new Error("Value and key cannot be undefined!");
+      }
+      values = (_ref = {}, _ref[key + ""] = val, _ref);
+    }
+    for (key in values) {
+      val = values[key];
+      _.unsetNested(this._tombstones, key, {
+        ensure: true
+      });
+      _.setNested(this._data, key, val, {
+        ensure: true
+      });
+    }
+    return this;
+  };
+  /**
+   * Delete the given key from this object's data dictionary and set a tombstone
+   * which ensures that future lookups do not cascade and thus see the key as
+   * `undefined`.
+   * 
+   * If the key is missing from the data dictionary the delete does not cascade,
+   * but the tombstone is still set.
+   * 
+   * @param {String} key Key to unset.
+   * @returns {undefined|*} If found, returns the old value, and otherwise `undefined`.
+   */
+  prototype.unset = function(key){
+    var old;
+    old = this.get(key);
+    _.unsetNested(this._data, key);
+    _.setNested(this._tombstones, key, TOMBSTONE, {
+      ensure: true
+    });
+    return old;
+  };
+  /**
+   * Unsets the key in the data dictionary, but ensures future lookups also
+   * see the key as `undefined`, as opposed.
+   * 
+   * @param {String} key Key to unset.
+   * @returns {this} 
+   */
+  prototype.inherit = function(key){
+    _.unsetNested(this._tombstones, key, {
+      ensure: true
+    });
+    return _.unsetNested(this._data, key);
+  };
+  prototype.extend = function(){
+    var o, _i, _len;
+    for (_i = 0, _len = arguments.length; _i < _len; ++_i) {
+      o = arguments[_i];
+      this.set(o);
+    }
+    return this;
+  };
+  /**
+   * Recursively collapses the Cascade to a plain object by recursively merging the
+   * lookups (in reverse order) into the data.
+   * @returns {Object}
+   */
+  prototype.collapse = function(){
+    var o, k;
+    o = _.merge.apply(_, [{}].concat(__slice.call(this._lookups.slice(1).reverse())));
+    for (k in this._tombstones) {
+      delete o[k];
+    }
+    return _.merge(o, this._data);
+  };
+  /**
+   * Returns a plain object for JSON serialization via {@link Cascade#collapse()}.
+   * The name of this method is a bit confusing, as it doesn't actually return a 
+   * JSON string -- but I'm afraid that it's the way that the JavaScript API for 
+   * `JSON.stringify()` works.
+   * 
+   * @see https://developer.mozilla.org/en/JSON#toJSON()_method
+   * @return {Object} Plain object for JSON serialization.
+   */
+  prototype.toJSON = function(){
+    return this.collapse();
+  };
+  prototype.keys = function(){
+    return _.flatten(_.map(this._lookups, function(it){
+      return _.keys(it);
+    }));
+  };
+  prototype.values = function(){
+    return _.flatten(_.map(this._lookups, function(it){
+      return _.values(it);
+    }));
+  };
+  prototype.reduce = function(fn, acc, context){
+    context == null && (context = this);
+    return _.reduce(this._lookups, fn, acc, context);
+  };
+  prototype.map = function(fn, context){
+    context == null && (context = this);
+    return _.map(this._lookups, fn, context);
+  };
+  prototype.filter = function(fn, context){
+    context == null && (context = this);
+    return _.filter(this._lookups, fn, context);
+  };
+  prototype.each = function(fn, context){
+    context == null && (context = this);
+    _.each(this._lookups, fn, context);
+    return this;
+  };
+  prototype.invoke = function(name){
+    var args;
+    args = __slice.call(arguments, 1);
+    return _.invoke.apply(_, [this._lookups, name].concat(__slice.call(args)));
+  };
+  prototype.pluck = function(attr){
+    return _.pluck(this._lookups, attr);
+  };
+  prototype.find = function(fn, context){
+    context == null && (context = this);
+    return _.find(this._lookups, fn, context);
+  };
+  prototype.toString = function(){
+    var Cls;
+    Cls = this.constructor;
+    return (Cls.displayName || Cls.name) + "()";
+  };
+  return Cascade;
+}());
+ALIASES = {
+  setTombstone: 'unset',
+  toObject: 'collapse',
+  forEach: 'each'
+};
+for (dest in ALIASES) {
+  src = ALIASES[dest];
+  Cascade.prototype[dest] = Cascade.prototype[src];
+}
+module.exports = exports = Cascade;
+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/util/event/index.js b/lib/util/event/index.js
new file mode 100644 (file)
index 0000000..7d57333
--- /dev/null
@@ -0,0 +1,2 @@
+exports.WaitingEmitter = require('kraken/util/event/waiting-emitter');
+exports.ReadyEmitter = require('kraken/util/event/ready-emitter');
\ No newline at end of file
diff --git a/lib/util/event/index.mod.js b/lib/util/event/index.mod.js
new file mode 100644 (file)
index 0000000..175958e
--- /dev/null
@@ -0,0 +1,6 @@
+require.define('/node_modules/kraken/util/event/index.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+exports.WaitingEmitter = require('kraken/util/event/waiting-emitter');
+exports.ReadyEmitter = require('kraken/util/event/ready-emitter');
+
+});
diff --git a/lib/util/event/ready-emitter.js b/lib/util/event/ready-emitter.js
new file mode 100644 (file)
index 0000000..76b12a5
--- /dev/null
@@ -0,0 +1,72 @@
+var Base, ReadyEmitter, exports;
+Base = require('kraken/base/base');
+/**
+ * @class An EventEmitter that auto-triggers new handlers once "ready".
+ */
+ReadyEmitter = (function(superclass){
+  ReadyEmitter.displayName = 'ReadyEmitter';
+  var prototype = __extend(ReadyEmitter, superclass).prototype, constructor = ReadyEmitter;
+  prototype.readyEventName = 'ready';
+  prototype.ready = false;
+  /**
+   * Triggers the 'ready' event if it has not yet been triggered.
+   * Subsequent listeners added to this event will be auto-triggered.
+   * @param {Boolean} [force=false] Trigger the event even if already ready.
+   * @returns {this}
+   */
+  prototype.triggerReady = function(force){
+    if (this.ready && !force) {
+      return this;
+    }
+    this.ready = true;
+    this.emit(this.readyEventName, this);
+    return this;
+  };
+  /**
+   * Resets the 'ready' event to its non-triggered state, firing a
+   * 'ready-reset' event.
+   * @param {Boolean} [force=false] Trigger the event even if already reset.
+   * @returns {this}
+   */
+  prototype.resetReady = function(force){
+    if (!(this.ready && !force)) {
+      return this;
+    }
+    this.ready = false;
+    this.emit(this.readyEventName + "-reset", this);
+    return this;
+  };
+  /**
+   * Wrap {@link EventEmitter#on} registration to handle registrations
+   * on 'ready' after we've broadcast the event. Handler will always still
+   * be registered, however, in case the emitter is reset.
+   * 
+   * @param {String} events Space-separated events for which to register.
+   * @param {Function} callback
+   * @param {Object} [context]
+   * @returns {this}
+   */
+  prototype.on = function(events, callback, context){
+    var _this = this;
+    context == null && (context = this);
+    if (!callback) {
+      return this;
+    }
+    superclass.prototype.on.apply(this, arguments);
+    if (this.ready && -1 !== events.split(/\s+/).indexOf(this.readyEventName)) {
+      setTimeout(function(){
+        return callback.call(context, _this);
+      });
+    }
+    return this;
+  };
+  function ReadyEmitter(){}
+  return ReadyEmitter;
+}(Base));
+module.exports = exports = 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/util/event/ready-emitter.mod.js b/lib/util/event/ready-emitter.mod.js
new file mode 100644 (file)
index 0000000..19ecfbf
--- /dev/null
@@ -0,0 +1,76 @@
+require.define('/node_modules/kraken/util/event/ready-emitter.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var Base, ReadyEmitter, exports;
+Base = require('kraken/base/base');
+/**
+ * @class An EventEmitter that auto-triggers new handlers once "ready".
+ */
+ReadyEmitter = (function(superclass){
+  ReadyEmitter.displayName = 'ReadyEmitter';
+  var prototype = __extend(ReadyEmitter, superclass).prototype, constructor = ReadyEmitter;
+  prototype.readyEventName = 'ready';
+  prototype.ready = false;
+  /**
+   * Triggers the 'ready' event if it has not yet been triggered.
+   * Subsequent listeners added to this event will be auto-triggered.
+   * @param {Boolean} [force=false] Trigger the event even if already ready.
+   * @returns {this}
+   */
+  prototype.triggerReady = function(force){
+    if (this.ready && !force) {
+      return this;
+    }
+    this.ready = true;
+    this.emit(this.readyEventName, this);
+    return this;
+  };
+  /**
+   * Resets the 'ready' event to its non-triggered state, firing a
+   * 'ready-reset' event.
+   * @param {Boolean} [force=false] Trigger the event even if already reset.
+   * @returns {this}
+   */
+  prototype.resetReady = function(force){
+    if (!(this.ready && !force)) {
+      return this;
+    }
+    this.ready = false;
+    this.emit(this.readyEventName + "-reset", this);
+    return this;
+  };
+  /**
+   * Wrap {@link EventEmitter#on} registration to handle registrations
+   * on 'ready' after we've broadcast the event. Handler will always still
+   * be registered, however, in case the emitter is reset.
+   * 
+   * @param {String} events Space-separated events for which to register.
+   * @param {Function} callback
+   * @param {Object} [context]
+   * @returns {this}
+   */
+  prototype.on = function(events, callback, context){
+    var _this = this;
+    context == null && (context = this);
+    if (!callback) {
+      return this;
+    }
+    superclass.prototype.on.apply(this, arguments);
+    if (this.ready && -1 !== events.split(/\s+/).indexOf(this.readyEventName)) {
+      setTimeout(function(){
+        return callback.call(context, _this);
+      });
+    }
+    return this;
+  };
+  function ReadyEmitter(){}
+  return ReadyEmitter;
+}(Base));
+module.exports = exports = 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/util/event/waiting-emitter.js b/lib/util/event/waiting-emitter.js
new file mode 100644 (file)
index 0000000..e87ccd8
--- /dev/null
@@ -0,0 +1,63 @@
+var Base, WaitingEmitter, exports;
+Base = require('kraken/base/base');
+/**
+ * @class An EventEmitter with a ratchet-up waiting counter.
+ * @extends Base
+ */
+WaitingEmitter = (function(superclass){
+  WaitingEmitter.displayName = 'WaitingEmitter';
+  var prototype = __extend(WaitingEmitter, superclass).prototype, constructor = WaitingEmitter;
+  /**
+   * Count of outstanding tasks.
+   * @type Number
+   */
+  prototype.waitingOn = 0;
+  /**
+   * Increment the waiting task counter.
+   * @returns {this}
+   */
+  prototype.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}
+   */
+  prototype.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.
+   */
+  prototype.unwaitAnd = function(fn){
+    var self;
+    self = this;
+    return function(){
+      self.unwait();
+      return fn.apply(this, arguments);
+    };
+  };
+  function WaitingEmitter(){}
+  return WaitingEmitter;
+}(Base));
+module.exports = exports = WaitingEmitter;
+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/util/event/waiting-emitter.mod.js b/lib/util/event/waiting-emitter.mod.js
new file mode 100644 (file)
index 0000000..4f17042
--- /dev/null
@@ -0,0 +1,67 @@
+require.define('/node_modules/kraken/util/event/waiting-emitter.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var Base, WaitingEmitter, exports;
+Base = require('kraken/base/base');
+/**
+ * @class An EventEmitter with a ratchet-up waiting counter.
+ * @extends Base
+ */
+WaitingEmitter = (function(superclass){
+  WaitingEmitter.displayName = 'WaitingEmitter';
+  var prototype = __extend(WaitingEmitter, superclass).prototype, constructor = WaitingEmitter;
+  /**
+   * Count of outstanding tasks.
+   * @type Number
+   */
+  prototype.waitingOn = 0;
+  /**
+   * Increment the waiting task counter.
+   * @returns {this}
+   */
+  prototype.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}
+   */
+  prototype.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.
+   */
+  prototype.unwaitAnd = function(fn){
+    var self;
+    self = this;
+    return function(){
+      self.unwait();
+      return fn.apply(this, arguments);
+    };
+  };
+  function WaitingEmitter(){}
+  return WaitingEmitter;
+}(Base));
+module.exports = exports = WaitingEmitter;
+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/util/formatters.js b/lib/util/formatters.js
new file mode 100644 (file)
index 0000000..4da7c06
--- /dev/null
@@ -0,0 +1,71 @@
+var moment, op, exports, _ref, _, _fmt;
+moment = require('moment');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_fmt = {
+  /**
+   * Formats a date for display on an axis: `MM/YYYY`
+   * @param {Date} d Date to format.
+   * @returns {String}
+   */
+  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}
+   */,
+  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.
+   */,
+  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;
+      }
+    };
+  },
+  numberFormatterHTML: function(n, digits){
+    var whole, fraction, suffix, _ref;
+    digits == null && (digits = 2);
+    _ref = _fmt._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>";
+  }
+};
+module.exports = exports = _fmt;
\ No newline at end of file
diff --git a/lib/util/formatters.mod.js b/lib/util/formatters.mod.js
new file mode 100644 (file)
index 0000000..04972b7
--- /dev/null
@@ -0,0 +1,75 @@
+require.define('/node_modules/kraken/util/formatters.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var moment, op, exports, _ref, _, _fmt;
+moment = require('moment');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+_fmt = {
+  /**
+   * Formats a date for display on an axis: `MM/YYYY`
+   * @param {Date} d Date to format.
+   * @returns {String}
+   */
+  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}
+   */,
+  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.
+   */,
+  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;
+      }
+    };
+  },
+  numberFormatterHTML: function(n, digits){
+    var whole, fraction, suffix, _ref;
+    digits == null && (digits = 2);
+    _ref = _fmt._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>";
+  }
+};
+module.exports = exports = _fmt;
+
+});
diff --git a/lib/util/index.js b/lib/util/index.js
new file mode 100644 (file)
index 0000000..2cc464f
--- /dev/null
@@ -0,0 +1,43 @@
+var op, root, backbone, parser, Cascade, _, _ref, __slice = [].slice;
+_ = exports._ = require('kraken/util/underscore');
+op = exports.op = require('kraken/util/op');
+root = exports.root = function(){
+  return this;
+}();
+root.console || (root.console = _(['log', 'info', 'warn', 'error', 'dir', 'table', 'group', 'groupCollapsed', 'groupEnd']).synthesize(function(it){
+  return [it, op.nop];
+}));
+/**
+ * @returns {Object} Object of the data from the form, via `.serializeArray()`.
+ */
+if ((_ref = root.jQuery) != null) {
+  _ref.fn.formData = function(){
+    return _.synthesize(this.serializeArray(), function(it){
+      return [it.name, it.value];
+    });
+  };
+}
+/**
+ * Invokes a jQuery method on each element, returning the array of the result.
+ * @returns {Array} Results.
+ */
+if ((_ref = root.jQuery) != null) {
+  _ref.fn.invoke = function(method){
+    var args, idx, el, _len, _ref, _results = [];
+    args = __slice.call(arguments, 1);
+    for (idx = 0, _len = this.length; idx < _len; ++idx) {
+      el = this[idx];
+      _results.push((_ref = jQuery(el))[method].apply(_ref, args));
+    }
+    return _results;
+  };
+}
+__import(exports, require('kraken/util/event'));
+backbone = exports.backbone = require('kraken/util/backbone');
+parser = exports.parser = require('kraken/util/parser');
+Cascade = exports.Cascade = require('kraken/util/cascade');
+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/util/index.mod.js b/lib/util/index.mod.js
new file mode 100644 (file)
index 0000000..2025d0c
--- /dev/null
@@ -0,0 +1,47 @@
+require.define('/node_modules/kraken/util/index.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var op, root, backbone, parser, Cascade, _, _ref, __slice = [].slice;
+_ = exports._ = require('kraken/util/underscore');
+op = exports.op = require('kraken/util/op');
+root = exports.root = function(){
+  return this;
+}();
+root.console || (root.console = _(['log', 'info', 'warn', 'error', 'dir', 'table', 'group', 'groupCollapsed', 'groupEnd']).synthesize(function(it){
+  return [it, op.nop];
+}));
+/**
+ * @returns {Object} Object of the data from the form, via `.serializeArray()`.
+ */
+if ((_ref = root.jQuery) != null) {
+  _ref.fn.formData = function(){
+    return _.synthesize(this.serializeArray(), function(it){
+      return [it.name, it.value];
+    });
+  };
+}
+/**
+ * Invokes a jQuery method on each element, returning the array of the result.
+ * @returns {Array} Results.
+ */
+if ((_ref = root.jQuery) != null) {
+  _ref.fn.invoke = function(method){
+    var args, idx, el, _len, _ref, _results = [];
+    args = __slice.call(arguments, 1);
+    for (idx = 0, _len = this.length; idx < _len; ++idx) {
+      el = this[idx];
+      _results.push((_ref = jQuery(el))[method].apply(_ref, args));
+    }
+    return _results;
+  };
+}
+__import(exports, require('kraken/util/event'));
+backbone = exports.backbone = require('kraken/util/backbone');
+parser = exports.parser = require('kraken/util/parser');
+Cascade = exports.Cascade = require('kraken/util/cascade');
+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/util/op.js b/lib/util/op.js
new file mode 100644 (file)
index 0000000..7bb2e07
--- /dev/null
@@ -0,0 +1,291 @@
+var DASH_PATTERN, STRIP_PAT, strip, FALSEY, parseBool, op, __slice = [].slice;
+DASH_PATTERN = /-/g;
+STRIP_PAT = /(^\s*|\s*$)/g;
+strip = function(s){
+  if (s) {
+    return s.replace(STRIP_PAT, '');
+  } else {
+    return s;
+  }
+};
+FALSEY = /^\s*(?:no|off|false)\s*$/i;
+parseBool = function(s){
+  var i;
+  i = parseInt(s || 0);
+  return !!(isNaN(i) ? !FALSEY.test(s) : i);
+};
+module.exports = op = {
+  I: function(x){
+    return x;
+  },
+  K: function(k){
+    return function(){
+      return k;
+    };
+  },
+  nop: function(){},
+  kThis: function(){
+    return this;
+  },
+  kObject: function(){
+    return {};
+  },
+  kArray: function(){
+    return [];
+  },
+  val: function(def, o){
+    return o != null ? o : def;
+  },
+  ok: function(o){
+    return o != null;
+  },
+  notOk: function(o){
+    return o == null;
+  },
+  first: function(a){
+    return a;
+  },
+  second: function(_, a){
+    return a;
+  },
+  nth: function(n){
+    switch (n) {
+    case 0:
+      return op.first;
+    case 1:
+      return op.second;
+    default:
+      return function(){
+        return arguments[n];
+      };
+    }
+  },
+  flip: function(fn){
+    return function(a, b){
+      arguments[0] = b;
+      arguments[1] = a;
+      return fn.apply(this, arguments);
+    };
+  },
+  aritize: function(fn, cxt, n){
+    var _ref;
+    if (arguments.length < 3) {
+      _ref = [cxt, null], n = _ref[0], cxt = _ref[1];
+    }
+    return function(){
+      return fn.apply(cxt != null ? cxt : this, [].slice.call(arguments, 0, n));
+    };
+  },
+  it: function(fn, cxt){
+    return function(it){
+      return fn.call(cxt != null ? cxt : this, it);
+    };
+  },
+  khas: function(k, o){
+    return k in o;
+  },
+  kget: function(k, o){
+    return o[k];
+  },
+  defkget: function(def, k, o){
+    if (k in o) {
+      return o[k];
+    } else {
+      return def;
+    }
+  },
+  thisget: function(k){
+    return this[k];
+  },
+  vkset: function(o, v, k){
+    if (o && k != null) {
+      o[k] = v;
+    }
+    return o;
+  },
+  has: function(o, k){
+    return k in o;
+  },
+  get: function(o, k){
+    return o[k];
+  },
+  getdef: function(o, k, def){
+    if (k in o) {
+      return o[k];
+    } else {
+      return def;
+    }
+  },
+  kvset: function(o, k, v){
+    if (o && k != null) {
+      o[k] = v;
+    }
+    return o;
+  },
+  thiskvset: function(k, v){
+    if (k != null) {
+      this[k] = v;
+    }
+    return this;
+  },
+  prop: function(k){
+    return function(o){
+      return o[k];
+    };
+  },
+  method: function(name){
+    var args;
+    args = __slice.call(arguments, 1);
+    return function(obj){
+      var _args;
+      _args = __slice.call(arguments, 1);
+      if (obj != null && obj[name]) {
+        return obj[name].apply(obj, args.concat(_args));
+      }
+    };
+  },
+  isK: function(k){
+    return function(v){
+      return v === k;
+    };
+  },
+  parseBool: parseBool,
+  toBool: parseBool,
+  toInt: function(v){
+    return parseInt(v);
+  },
+  toFloat: function(v){
+    return parseFloat(v);
+  },
+  toStr: function(v){
+    return String(v);
+  },
+  toRegExp: function(v){
+    return new RegExp(v);
+  },
+  toObject: function(v){
+    if (typeof v === 'string' && strip(v)) {
+      return JSON.parse(v);
+    } else {
+      return v;
+    }
+  },
+  toDate: function(v){
+    if (v == null || v instanceof Date) {
+      return v;
+    }
+    if (typeof v === 'number') {
+      return new Date(v);
+    }
+    return new Date(String(v).replace(DASH_PATTERN, '/'));
+  },
+  cmp: function(x, y){
+    if (x < y) {
+      return -1;
+    } else {
+      return x > y ? 1 : 0;
+    }
+  },
+  eq: function(x, y){
+    return x == y;
+  },
+  ne: function(x, y){
+    return x != y;
+  },
+  gt: function(x, y){
+    return x > y;
+  },
+  ge: function(x, y){
+    return x >= y;
+  },
+  lt: function(x, y){
+    return x < y;
+  },
+  le: function(x, y){
+    return x <= y;
+  },
+  add: function(x, y){
+    return x + y;
+  },
+  sub: function(x, y){
+    return x - y;
+  },
+  mul: function(x, y){
+    return x * y;
+  },
+  div: function(x, y){
+    return x / y;
+  },
+  flrdiv: function(x, y){
+    return Math.floor(x / y);
+  },
+  mod: function(x, y){
+    return x % y;
+  },
+  neg: function(x){
+    return -x;
+  },
+  log2: function(n){
+    return Math.log(n / Math.LN2);
+  },
+  is: function(x, y){
+    return x === y;
+  },
+  isnt: function(x, y){
+    return x !== y;
+  },
+  and: function(x, y){
+    return x && y;
+  },
+  or: function(x, y){
+    return x || y;
+  },
+  not: function(x){
+    return !x;
+  },
+  bitnot: function(x){
+    return ~x;
+  },
+  bitand: function(x, y){
+    return x & y;
+  },
+  bitor: function(x, y){
+    return x | y;
+  },
+  bitxor: function(x, y){
+    return x ^ y;
+  },
+  lshift: function(x, y){
+    return x << y;
+  },
+  rshift: function(x, y){
+    return x >> y;
+  },
+  bin: function(n){
+    var s;
+    do {
+      s = (n % 2 ? '1' : '0') + (s || '');
+      n >>= 1;
+    } while (n);
+    return s;
+  },
+  binlen: function(n){
+    return bin(Math.abs(n)).length;
+  },
+  mask: function(n){
+    return (1 << n) - 1;
+  },
+  chr: function(it){
+    return String.fromCharCode(it);
+  },
+  ord: function(it){
+    return String(it).charCodeAt(0);
+  },
+  encode: function(it){
+    return it && $("<div>" + it + "</div>").html().replace(/"/g, '&quot;');
+  },
+  decode: function(it){
+    return it && $("<div>" + it + "</div>").text();
+  },
+  strip: strip
+};
\ No newline at end of file
diff --git a/lib/util/op.mod.js b/lib/util/op.mod.js
new file mode 100644 (file)
index 0000000..a918615
--- /dev/null
@@ -0,0 +1,295 @@
+require.define('/node_modules/kraken/util/op.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var DASH_PATTERN, STRIP_PAT, strip, FALSEY, parseBool, op, __slice = [].slice;
+DASH_PATTERN = /-/g;
+STRIP_PAT = /(^\s*|\s*$)/g;
+strip = function(s){
+  if (s) {
+    return s.replace(STRIP_PAT, '');
+  } else {
+    return s;
+  }
+};
+FALSEY = /^\s*(?:no|off|false)\s*$/i;
+parseBool = function(s){
+  var i;
+  i = parseInt(s || 0);
+  return !!(isNaN(i) ? !FALSEY.test(s) : i);
+};
+module.exports = op = {
+  I: function(x){
+    return x;
+  },
+  K: function(k){
+    return function(){
+      return k;
+    };
+  },
+  nop: function(){},
+  kThis: function(){
+    return this;
+  },
+  kObject: function(){
+    return {};
+  },
+  kArray: function(){
+    return [];
+  },
+  val: function(def, o){
+    return o != null ? o : def;
+  },
+  ok: function(o){
+    return o != null;
+  },
+  notOk: function(o){
+    return o == null;
+  },
+  first: function(a){
+    return a;
+  },
+  second: function(_, a){
+    return a;
+  },
+  nth: function(n){
+    switch (n) {
+    case 0:
+      return op.first;
+    case 1:
+      return op.second;
+    default:
+      return function(){
+        return arguments[n];
+      };
+    }
+  },
+  flip: function(fn){
+    return function(a, b){
+      arguments[0] = b;
+      arguments[1] = a;
+      return fn.apply(this, arguments);
+    };
+  },
+  aritize: function(fn, cxt, n){
+    var _ref;
+    if (arguments.length < 3) {
+      _ref = [cxt, null], n = _ref[0], cxt = _ref[1];
+    }
+    return function(){
+      return fn.apply(cxt != null ? cxt : this, [].slice.call(arguments, 0, n));
+    };
+  },
+  it: function(fn, cxt){
+    return function(it){
+      return fn.call(cxt != null ? cxt : this, it);
+    };
+  },
+  khas: function(k, o){
+    return k in o;
+  },
+  kget: function(k, o){
+    return o[k];
+  },
+  defkget: function(def, k, o){
+    if (k in o) {
+      return o[k];
+    } else {
+      return def;
+    }
+  },
+  thisget: function(k){
+    return this[k];
+  },
+  vkset: function(o, v, k){
+    if (o && k != null) {
+      o[k] = v;
+    }
+    return o;
+  },
+  has: function(o, k){
+    return k in o;
+  },
+  get: function(o, k){
+    return o[k];
+  },
+  getdef: function(o, k, def){
+    if (k in o) {
+      return o[k];
+    } else {
+      return def;
+    }
+  },
+  kvset: function(o, k, v){
+    if (o && k != null) {
+      o[k] = v;
+    }
+    return o;
+  },
+  thiskvset: function(k, v){
+    if (k != null) {
+      this[k] = v;
+    }
+    return this;
+  },
+  prop: function(k){
+    return function(o){
+      return o[k];
+    };
+  },
+  method: function(name){
+    var args;
+    args = __slice.call(arguments, 1);
+    return function(obj){
+      var _args;
+      _args = __slice.call(arguments, 1);
+      if (obj != null && obj[name]) {
+        return obj[name].apply(obj, args.concat(_args));
+      }
+    };
+  },
+  isK: function(k){
+    return function(v){
+      return v === k;
+    };
+  },
+  parseBool: parseBool,
+  toBool: parseBool,
+  toInt: function(v){
+    return parseInt(v);
+  },
+  toFloat: function(v){
+    return parseFloat(v);
+  },
+  toStr: function(v){
+    return String(v);
+  },
+  toRegExp: function(v){
+    return new RegExp(v);
+  },
+  toObject: function(v){
+    if (typeof v === 'string' && strip(v)) {
+      return JSON.parse(v);
+    } else {
+      return v;
+    }
+  },
+  toDate: function(v){
+    if (v == null || v instanceof Date) {
+      return v;
+    }
+    if (typeof v === 'number') {
+      return new Date(v);
+    }
+    return new Date(String(v).replace(DASH_PATTERN, '/'));
+  },
+  cmp: function(x, y){
+    if (x < y) {
+      return -1;
+    } else {
+      return x > y ? 1 : 0;
+    }
+  },
+  eq: function(x, y){
+    return x == y;
+  },
+  ne: function(x, y){
+    return x != y;
+  },
+  gt: function(x, y){
+    return x > y;
+  },
+  ge: function(x, y){
+    return x >= y;
+  },
+  lt: function(x, y){
+    return x < y;
+  },
+  le: function(x, y){
+    return x <= y;
+  },
+  add: function(x, y){
+    return x + y;
+  },
+  sub: function(x, y){
+    return x - y;
+  },
+  mul: function(x, y){
+    return x * y;
+  },
+  div: function(x, y){
+    return x / y;
+  },
+  flrdiv: function(x, y){
+    return Math.floor(x / y);
+  },
+  mod: function(x, y){
+    return x % y;
+  },
+  neg: function(x){
+    return -x;
+  },
+  log2: function(n){
+    return Math.log(n / Math.LN2);
+  },
+  is: function(x, y){
+    return x === y;
+  },
+  isnt: function(x, y){
+    return x !== y;
+  },
+  and: function(x, y){
+    return x && y;
+  },
+  or: function(x, y){
+    return x || y;
+  },
+  not: function(x){
+    return !x;
+  },
+  bitnot: function(x){
+    return ~x;
+  },
+  bitand: function(x, y){
+    return x & y;
+  },
+  bitor: function(x, y){
+    return x | y;
+  },
+  bitxor: function(x, y){
+    return x ^ y;
+  },
+  lshift: function(x, y){
+    return x << y;
+  },
+  rshift: function(x, y){
+    return x >> y;
+  },
+  bin: function(n){
+    var s;
+    do {
+      s = (n % 2 ? '1' : '0') + (s || '');
+      n >>= 1;
+    } while (n);
+    return s;
+  },
+  binlen: function(n){
+    return bin(Math.abs(n)).length;
+  },
+  mask: function(n){
+    return (1 << n) - 1;
+  },
+  chr: function(it){
+    return String.fromCharCode(it);
+  },
+  ord: function(it){
+    return String(it).charCodeAt(0);
+  },
+  encode: function(it){
+    return it && $("<div>" + it + "</div>").html().replace(/"/g, '&quot;');
+  },
+  decode: function(it){
+    return it && $("<div>" + it + "</div>").text();
+  },
+  strip: strip
+};
+
+});
diff --git a/lib/util/parser.js b/lib/util/parser.js
new file mode 100644 (file)
index 0000000..8a6076d
--- /dev/null
@@ -0,0 +1,173 @@
+var op, BaseModel, BaseList, BaseView, Mixin, Parsers, ParserMixin, ParsingModel, ParsingList, ParsingView, _, _ref;
+_ = require('kraken/util/underscore');
+op = require('kraken/util/op');
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView, Mixin = _ref.Mixin;
+/**
+ * @namespace Parsers by type.
+ */
+Parsers = exports.Parsers = {
+  parseBoolean: function(v){
+    return op.toBool(v);
+  },
+  parseInteger: function(v){
+    var r;
+    r = op.toInt(v);
+    if (!isNaN(r)) {
+      return r;
+    } else {
+      return null;
+    }
+  },
+  parseFloat: function(v){
+    var r;
+    r = op.toFloat(v);
+    if (!isNaN(r)) {
+      return r;
+    } else {
+      return null;
+    }
+  },
+  parseString: function(v){
+    if (v != null) {
+      return op.toStr(v);
+    } else {
+      return null;
+    }
+  },
+  parseDate: function(v){
+    if (v) {
+      return op.toDate(v);
+    } else {
+      return null;
+    }
+  },
+  parseRegExp: function(v){
+    if (v) {
+      return op.toRegExp(v);
+    } else {
+      return null;
+    }
+  },
+  parseArray: function(v){
+    if (v) {
+      return op.toObject(v);
+    } else {
+      return null;
+    }
+  },
+  parseObject: function(v){
+    if (v) {
+      return op.toObject(v);
+    } else {
+      return null;
+    }
+  },
+  parseFunction: function(v){
+    if (v && _.startswith(String(v), 'function')) {
+      try {
+        return eval("(" + v + ")");
+      } catch (err) {
+        return null;
+      }
+    } else {
+      return null;
+    }
+  }
+};
+Parsers.parseNumber = Parsers.parseFloat;
+/**
+ * @class Methods for a class to select parsers by type reflection.
+ * @mixin
+ */
+exports.ParserMixin = ParserMixin = (function(superclass){
+  ParserMixin.displayName = 'ParserMixin';
+  var prototype = __extend(ParserMixin, superclass).prototype, constructor = ParserMixin;
+  __import(ParserMixin.prototype, Parsers);
+  function ParserMixin(target){
+    return Mixin.call(ParserMixin, target);
+  }
+  prototype.parseValue = function(v, type){
+    return this.getParser(type)(v);
+  };
+  prototype.getParser = function(type){
+    var fn, t, _i, _ref, _len;
+    type == null && (type = 'String');
+    fn = this["parse" + type];
+    if (typeof fn === 'function') {
+      return fn;
+    }
+    type = _(String(type).toLowerCase());
+    for (_i = 0, _len = (_ref = ['Integer', 'Float', 'Number', 'Boolean', 'Object', 'Array', 'Function']).length; _i < _len; ++_i) {
+      t = _ref[_i];
+      if (type.startsWith(t.toLowerCase())) {
+        return this["parse" + t];
+      }
+    }
+    return this.defaultParser || this.parseString;
+  };
+  prototype.getParserFromExample = function(v){
+    var type;
+    if (v == null) {
+      return null;
+    }
+    type = typeof v;
+    if (type !== 'object') {
+      return this.getParser(type);
+    } else if (_.isArray(v)) {
+      return this.getParser('Array');
+    } else {
+      return this.getParser('Object');
+    }
+  };
+  return ParserMixin;
+}(Mixin));
+/**
+ * @class Basic model which mixes in the ParserMixin.
+ * @extends BaseModel
+ * @borrows ParserMixin
+ */
+ParsingModel = exports.ParsingModel = BaseModel.extend(ParserMixin.mix({
+  constructor: (function(){
+    function ParsingModel(){
+      return BaseModel.apply(this, arguments);
+    }
+    return ParsingModel;
+  }())
+}));
+/**
+ * @class Basic collection which mixes in the ParserMixin.
+ * @extends BaseList
+ * @borrows ParserMixin
+ */
+ParsingList = exports.ParsingList = BaseList.extend(ParserMixin.mix({
+  constructor: (function(){
+    function ParsingList(){
+      return BaseList.apply(this, arguments);
+    }
+    return ParsingList;
+  }())
+}));
+/**
+ * @class Basic view which mixes in the ParserMixin.
+ * @extends BaseView
+ * @borrows ParserMixin
+ */
+ParsingView = exports.ParsingView = BaseView.extend(ParserMixin.mix({
+  constructor: (function(){
+    function ParsingView(){
+      return BaseView.apply(this, arguments);
+    }
+    return ParsingView;
+  }())
+}));
+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/util/parser.mod.js b/lib/util/parser.mod.js
new file mode 100644 (file)
index 0000000..5abf358
--- /dev/null
@@ -0,0 +1,177 @@
+require.define('/node_modules/kraken/util/parser.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var op, BaseModel, BaseList, BaseView, Mixin, Parsers, ParserMixin, ParsingModel, ParsingList, ParsingView, _, _ref;
+_ = require('kraken/util/underscore');
+op = require('kraken/util/op');
+_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList, BaseView = _ref.BaseView, Mixin = _ref.Mixin;
+/**
+ * @namespace Parsers by type.
+ */
+Parsers = exports.Parsers = {
+  parseBoolean: function(v){
+    return op.toBool(v);
+  },
+  parseInteger: function(v){
+    var r;
+    r = op.toInt(v);
+    if (!isNaN(r)) {
+      return r;
+    } else {
+      return null;
+    }
+  },
+  parseFloat: function(v){
+    var r;
+    r = op.toFloat(v);
+    if (!isNaN(r)) {
+      return r;
+    } else {
+      return null;
+    }
+  },
+  parseString: function(v){
+    if (v != null) {
+      return op.toStr(v);
+    } else {
+      return null;
+    }
+  },
+  parseDate: function(v){
+    if (v) {
+      return op.toDate(v);
+    } else {
+      return null;
+    }
+  },
+  parseRegExp: function(v){
+    if (v) {
+      return op.toRegExp(v);
+    } else {
+      return null;
+    }
+  },
+  parseArray: function(v){
+    if (v) {
+      return op.toObject(v);
+    } else {
+      return null;
+    }
+  },
+  parseObject: function(v){
+    if (v) {
+      return op.toObject(v);
+    } else {
+      return null;
+    }
+  },
+  parseFunction: function(v){
+    if (v && _.startswith(String(v), 'function')) {
+      try {
+        return eval("(" + v + ")");
+      } catch (err) {
+        return null;
+      }
+    } else {
+      return null;
+    }
+  }
+};
+Parsers.parseNumber = Parsers.parseFloat;
+/**
+ * @class Methods for a class to select parsers by type reflection.
+ * @mixin
+ */
+exports.ParserMixin = ParserMixin = (function(superclass){
+  ParserMixin.displayName = 'ParserMixin';
+  var prototype = __extend(ParserMixin, superclass).prototype, constructor = ParserMixin;
+  __import(ParserMixin.prototype, Parsers);
+  function ParserMixin(target){
+    return Mixin.call(ParserMixin, target);
+  }
+  prototype.parseValue = function(v, type){
+    return this.getParser(type)(v);
+  };
+  prototype.getParser = function(type){
+    var fn, t, _i, _ref, _len;
+    type == null && (type = 'String');
+    fn = this["parse" + type];
+    if (typeof fn === 'function') {
+      return fn;
+    }
+    type = _(String(type).toLowerCase());
+    for (_i = 0, _len = (_ref = ['Integer', 'Float', 'Number', 'Boolean', 'Object', 'Array', 'Function']).length; _i < _len; ++_i) {
+      t = _ref[_i];
+      if (type.startsWith(t.toLowerCase())) {
+        return this["parse" + t];
+      }
+    }
+    return this.defaultParser || this.parseString;
+  };
+  prototype.getParserFromExample = function(v){
+    var type;
+    if (v == null) {
+      return null;
+    }
+    type = typeof v;
+    if (type !== 'object') {
+      return this.getParser(type);
+    } else if (_.isArray(v)) {
+      return this.getParser('Array');
+    } else {
+      return this.getParser('Object');
+    }
+  };
+  return ParserMixin;
+}(Mixin));
+/**
+ * @class Basic model which mixes in the ParserMixin.
+ * @extends BaseModel
+ * @borrows ParserMixin
+ */
+ParsingModel = exports.ParsingModel = BaseModel.extend(ParserMixin.mix({
+  constructor: (function(){
+    function ParsingModel(){
+      return BaseModel.apply(this, arguments);
+    }
+    return ParsingModel;
+  }())
+}));
+/**
+ * @class Basic collection which mixes in the ParserMixin.
+ * @extends BaseList
+ * @borrows ParserMixin
+ */
+ParsingList = exports.ParsingList = BaseList.extend(ParserMixin.mix({
+  constructor: (function(){
+    function ParsingList(){
+      return BaseList.apply(this, arguments);
+    }
+    return ParsingList;
+  }())
+}));
+/**
+ * @class Basic view which mixes in the ParserMixin.
+ * @extends BaseView
+ * @borrows ParserMixin
+ */
+ParsingView = exports.ParsingView = BaseView.extend(ParserMixin.mix({
+  constructor: (function(){
+    function ParsingView(){
+      return BaseView.apply(this, arguments);
+    }
+    return ParsingView;
+  }())
+}));
+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/util/timeseries/csv.js b/lib/util/timeseries/csv.js
new file mode 100644 (file)
index 0000000..91b31d4
--- /dev/null
@@ -0,0 +1,133 @@
+var TimeSeriesData, DASH_PATTERN, BLANK_LINE_PATTERN, COMMENT_PATTERN, CSVData, exports, _;
+_ = require('kraken/util/underscore');
+TimeSeriesData = require('kraken/util/timeseries/timeseries');
+DASH_PATTERN = /-/g;
+BLANK_LINE_PATTERN = /^(\s*)$/;
+COMMENT_PATTERN = /\s*(#|\/\/).*$/;
+CSVData = (function(superclass){
+  CSVData.displayName = 'CSVData';
+  var prototype = __extend(CSVData, superclass).prototype, constructor = CSVData;
+  prototype.DEFAULT_OPTIONS = {
+    colSep: ',',
+    rowSep: '\n',
+    defaultType: 'float',
+    customBars: false,
+    customSep: ';',
+    errorBars: false,
+    fractions: false,
+    fractionSep: '/',
+    skipBlankLines: true,
+    blankLinePat: BLANK_LINE_PATTERN,
+    removeCommentedText: true,
+    commentPat: COMMENT_PATTERN,
+    replaceMissing: false,
+    replaceMissingValue: 0,
+    replaceNaN: false,
+    replaceNaNValue: 0,
+    padRows: false,
+    padRowsValue: 0
+  };
+  function CSVData(data, opts){
+    superclass.apply(this, arguments);
+  }
+  /* * * *  CSV Parsing  * * * */
+  prototype.parseNumber = function(s){
+    return parseFloat(s);
+  };
+  prototype.parseHiLo = function(s){
+    return s.split(this.options.customBars).map(this.parseNumber, this);
+  };
+  prototype.parseFraction = function(s){
+    return s.split(this.options.fractionSep).map(this.parseNumber, this);
+  };
+  prototype.parseDate = function(s){
+    return new Date(s.replace(DASH_PATTERN, '/'));
+  };
+  /**
+   * Parses and imports a CSV string.
+   * 
+   * @private
+   * @returns {this}
+   */
+  prototype.parseData = function(rawData){
+    var o, lines, first, delim, rows, parser, hasHeaders, i, line, cols, date, fields, _len, _this = this;
+    this.rawData = rawData;
+    if (typeof rawData !== 'string') {
+      return this;
+    }
+    o = this.options;
+    lines = rawData.split(o.rowSep);
+    if (!lines.length) {
+      return [];
+    }
+    first = lines[0];
+    delim = o.colSep;
+    if (first.indexOf(delim) === -1 && first.indexOf('\t') >= 0) {
+      delim = '\t';
+    }
+    rows = this.rows = [];
+    this.columns = [];
+    parser = this.parseNumber;
+    if (o.customBars) {
+      parser = this.parseHiLo;
+    }
+    if (o.fractions) {
+      parser = this.parseFraction;
+    }
+    hasHeaders = this.labels.length !== 0;
+    for (i = 0, _len = lines.length; i < _len; ++i) {
+      line = lines[i];
+      if (o.removeCommentedText) {
+        line = line.replace(o.commentPat, '');
+      }
+      if (o.skipBlankLines && (line.length === 0 || o.blankLinePat.test(line))) {
+        continue;
+      }
+      cols = line.split(delim);
+      if (!hasHeaders) {
+        hasHeaders = true;
+        this.labels = cols.map(_fn);
+        continue;
+      }
+      if (!(cols.length > 1)) {
+        continue;
+      }
+      date = this.parseDate(cols.shift());
+      fields = cols.map(parser, this);
+      if (o.errorBars) {
+        fields = fields.reduce(_fn2, []);
+      }
+      fields.unshift(date);
+      rows.push(fields);
+      fields.forEach(_fn3);
+    }
+    this.untransformedRows = _.merge([], this.rows);
+    return this;
+    function _fn(it){
+      return _.strip(it);
+    }
+    function _fn2(acc, v){
+      var last;
+      last = acc[acc.length - 1];
+      if (!(last && last.length < 2)) {
+        acc.push(last = []);
+      }
+      last.push(v);
+      return acc;
+    }
+    function _fn3(v, idx){
+      if (!_this.columns[idx]) {
+        _this.columns.push([]);
+      }
+      return _this.columns[idx].push(v);
+    }
+  };
+  return CSVData;
+}(TimeSeriesData));
+module.exports = exports = CSVData;
+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/util/timeseries/csv.mod.js b/lib/util/timeseries/csv.mod.js
new file mode 100644 (file)
index 0000000..15debc2
--- /dev/null
@@ -0,0 +1,137 @@
+require.define('/node_modules/kraken/util/timeseries/csv.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var TimeSeriesData, DASH_PATTERN, BLANK_LINE_PATTERN, COMMENT_PATTERN, CSVData, exports, _;
+_ = require('kraken/util/underscore');
+TimeSeriesData = require('kraken/util/timeseries/timeseries');
+DASH_PATTERN = /-/g;
+BLANK_LINE_PATTERN = /^(\s*)$/;
+COMMENT_PATTERN = /\s*(#|\/\/).*$/;
+CSVData = (function(superclass){
+  CSVData.displayName = 'CSVData';
+  var prototype = __extend(CSVData, superclass).prototype, constructor = CSVData;
+  prototype.DEFAULT_OPTIONS = {
+    colSep: ',',
+    rowSep: '\n',
+    defaultType: 'float',
+    customBars: false,
+    customSep: ';',
+    errorBars: false,
+    fractions: false,
+    fractionSep: '/',
+    skipBlankLines: true,
+    blankLinePat: BLANK_LINE_PATTERN,
+    removeCommentedText: true,
+    commentPat: COMMENT_PATTERN,
+    replaceMissing: false,
+    replaceMissingValue: 0,
+    replaceNaN: false,
+    replaceNaNValue: 0,
+    padRows: false,
+    padRowsValue: 0
+  };
+  function CSVData(data, opts){
+    superclass.apply(this, arguments);
+  }
+  /* * * *  CSV Parsing  * * * */
+  prototype.parseNumber = function(s){
+    return parseFloat(s);
+  };
+  prototype.parseHiLo = function(s){
+    return s.split(this.options.customBars).map(this.parseNumber, this);
+  };
+  prototype.parseFraction = function(s){
+    return s.split(this.options.fractionSep).map(this.parseNumber, this);
+  };
+  prototype.parseDate = function(s){
+    return new Date(s.replace(DASH_PATTERN, '/'));
+  };
+  /**
+   * Parses and imports a CSV string.
+   * 
+   * @private
+   * @returns {this}
+   */
+  prototype.parseData = function(rawData){
+    var o, lines, first, delim, rows, parser, hasHeaders, i, line, cols, date, fields, _len, _this = this;
+    this.rawData = rawData;
+    if (typeof rawData !== 'string') {
+      return this;
+    }
+    o = this.options;
+    lines = rawData.split(o.rowSep);
+    if (!lines.length) {
+      return [];
+    }
+    first = lines[0];
+    delim = o.colSep;
+    if (first.indexOf(delim) === -1 && first.indexOf('\t') >= 0) {
+      delim = '\t';
+    }
+    rows = this.rows = [];
+    this.columns = [];
+    parser = this.parseNumber;
+    if (o.customBars) {
+      parser = this.parseHiLo;
+    }
+    if (o.fractions) {
+      parser = this.parseFraction;
+    }
+    hasHeaders = this.labels.length !== 0;
+    for (i = 0, _len = lines.length; i < _len; ++i) {
+      line = lines[i];
+      if (o.removeCommentedText) {
+        line = line.replace(o.commentPat, '');
+      }
+      if (o.skipBlankLines && (line.length === 0 || o.blankLinePat.test(line))) {
+        continue;
+      }
+      cols = line.split(delim);
+      if (!hasHeaders) {
+        hasHeaders = true;
+        this.labels = cols.map(_fn);
+        continue;
+      }
+      if (!(cols.length > 1)) {
+        continue;
+      }
+      date = this.parseDate(cols.shift());
+      fields = cols.map(parser, this);
+      if (o.errorBars) {
+        fields = fields.reduce(_fn2, []);
+      }
+      fields.unshift(date);
+      rows.push(fields);
+      fields.forEach(_fn3);
+    }
+    this.untransformedRows = _.merge([], this.rows);
+    return this;
+    function _fn(it){
+      return _.strip(it);
+    }
+    function _fn2(acc, v){
+      var last;
+      last = acc[acc.length - 1];
+      if (!(last && last.length < 2)) {
+        acc.push(last = []);
+      }
+      last.push(v);
+      return acc;
+    }
+    function _fn3(v, idx){
+      if (!_this.columns[idx]) {
+        _this.columns.push([]);
+      }
+      return _this.columns[idx].push(v);
+    }
+  };
+  return CSVData;
+}(TimeSeriesData));
+module.exports = exports = CSVData;
+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/util/timeseries/index.js b/lib/util/timeseries/index.js
new file mode 100644 (file)
index 0000000..bca1a5d
--- /dev/null
@@ -0,0 +1,2 @@
+exports.TimeSeriesData = require('kraken/util/timeseries/timeseries');
+exports.CSVData = require('kraken/util/timeseries/csv');
\ No newline at end of file
diff --git a/lib/util/timeseries/index.mod.js b/lib/util/timeseries/index.mod.js
new file mode 100644 (file)
index 0000000..8509f3e
--- /dev/null
@@ -0,0 +1,6 @@
+require.define('/node_modules/kraken/util/timeseries/index.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+exports.TimeSeriesData = require('kraken/util/timeseries/timeseries');
+exports.CSVData = require('kraken/util/timeseries/csv');
+
+});
diff --git a/lib/util/timeseries/timeseries.js b/lib/util/timeseries/timeseries.js
new file mode 100644 (file)
index 0000000..04b4702
--- /dev/null
@@ -0,0 +1,199 @@
+var TimeSeriesData, exports, _;
+_ = require('kraken/util/underscore');
+/**
+ * @class Represents a collection of data columns aligned along a common timeline.
+ */
+TimeSeriesData = (function(){
+  TimeSeriesData.displayName = 'TimeSeriesData';
+  var prototype = TimeSeriesData.prototype, constructor = TimeSeriesData;
+  prototype.DEFAULT_OPTIONS = {};
+  prototype.options = {};
+  prototype.labels = [];
+  prototype.types = [];
+  prototype.untransformedRows = null;
+  prototype.rows = null;
+  prototype.columns = null;
+  prototype.dateColumn = null;
+  prototype.dataColumns = null;
+  /**
+   * @constructor
+   */;
+  function TimeSeriesData(data, opts){
+    var that, _ref;
+    if (!(typeof data === 'string' || _.isArray(data))) {
+      _ref = [data, null], opts = _ref[0], data = _ref[1];
+    }
+    this.options = __import(_.clone(this.DEFAULT_OPTIONS), opts || {});
+    this.transforms = [];
+    this.labels = this.options.labels || [];
+    this.types = this.options.types || [];
+    if (that = data || this.options.data) {
+      this.parseData(that);
+    }
+    this.rebuildDerived();
+  }
+  /* * * *  TimeSeriesData interface  * * * */
+  /**
+   * @returns {Array<Array>} List of rows, each of which includes all columns.
+   */
+  prototype.getData = function(){
+    return this.data;
+  };
+  /**
+   * @returns {Array<Array>} List of all columns (including date column).
+   */
+  prototype.getColumns = function(){
+    return this.columns;
+  };
+  /**
+   * @returns {Array<Date>} The date column.
+   */
+  prototype.getDateColumn = function(){
+    return this.dateColumn;
+  };
+  /**
+   * @returns {Array<Array>} List of all columns except the date column.
+   */
+  prototype.getDataColumns = function(){
+    return this.dataColumns;
+  };
+  /**
+   * @returns {Array<String>} List of column labels.
+   */
+  prototype.getLabels = function(){
+    return this.labels;
+  };
+  /* * * *  Parsing  * * * */
+  /**
+   * Subclass and override to perform preprocessing of the data.
+   * @private
+   */
+  prototype.parseData = function(rawData){
+    return this;
+  };
+  /**
+   * Rebuilds the row-oriented data matrix from the columns.
+   * @private
+   */
+  prototype.rebuildData = function(){
+    this.rows = _.zip.apply(_, this.columns);
+    return this.rebuildDerived();
+  };
+  /**
+   * Rebuilds the column-oriented data matrix from the columns.
+   * @private
+   */
+  prototype.rebuildColumns = function(){
+    this.columns = _.zip.apply(_, this.rows);
+    return this.rebuildDerived();
+  };
+  /**
+   * @private
+   */
+  prototype.rebuildDerived = function(){
+    while (this.transforms.length < this.columns.length) {
+      this.transforms.push([]);
+    }
+    this.dateColumn = this.columns[0];
+    this.dataColumns = this.columns.slice(1);
+    return this;
+  };
+  /* * * *  Data Transformation  * * * */
+  /**
+   * Applies the stack of transforms to the data.
+   * 
+   * TODO: Apply transforms in @getData()?
+   * @private
+   * @returns {this}
+   */
+  prototype.applyTransforms = function(){
+    var idx, fns, fn, ctx, _ref, _len, _i, _len2, _ref2;
+    for (idx = 0, _len = (_ref = this.transforms).length; idx < _len; ++idx) {
+      fns = _ref[idx];
+      for (_i = 0, _len2 = fns.length; _i < _len2; ++_i) {
+        _ref2 = fns[_i], fn = _ref2[0], ctx = _ref2[1];
+        (_ref2 = this.columns)[idx] = _ref2[idx].map(fn, ctx);
+      }
+    }
+    return this.rebuildData();
+  };
+  /**
+   * Clears all transforms and restores the original data.
+   * @returns {this}
+   */
+  prototype.clearTransforms = function(){
+    this.transforms = [];
+    this.rows = _.merge([], this.untransformedRows);
+    return this.rebuildColumns();
+  };
+  /**
+   * Add a data transform to the specified columns. The function is
+   * applied one-by-one (in column-major order), replacing the data
+   * with the mapped result.
+   * 
+   * @param {Number|Array} indices List one or more column indices to map. Negative
+   *  numbers are offset from the end of the columns list.
+   * @param {Function} fn Mapping function of the form:
+   *  `(single_value, row_idx, column) -> new_value`
+   * @param {Object} [ctx=this] Execution context for the function.
+   * @returns {this}
+   */
+  prototype.addTransform = function(indices, fn, ctx){
+    var num_cols, idx, _ref, _i, _len;
+    ctx == null && (ctx = this);
+    num_cols = this.columns.length;
+    if (typeof idx === 'function') {
+      _ref = [fn, indices, null], ctx = _ref[0], fn = _ref[1], indices = _ref[2];
+    }
+    if (indices == null) {
+      indices = _.range(num_cols);
+    }
+    if (!_.isArray(indices)) {
+      indices = [indices];
+    }
+    for (_i = 0, _len = indices.length; _i < _len; ++_i) {
+      idx = indices[_i];
+      idx %= num_cols;
+      if (idx < 0) {
+        idx += num_cols;
+      }
+      this.transforms[idx].push([fn, ctx]);
+    }
+    return this.applyTransforms();
+  };
+  /**
+   * Add a data transform to all columns except the date column. The function
+   * is applied one-by-one (in column-major order), replacing the data
+   * with the mapped result.
+   * 
+   * @param {Function} fn Mapping function of the form:
+   *  `(single_value, row_idx, column) -> new_value`
+   * @param {Object} [ctx=this] Execution context for the function.
+   * @returns {this}
+   */
+  prototype.addDataTransform = function(fn, ctx){
+    ctx == null && (ctx = this);
+    return this.addTransform(_.range(1, this.columns.length), fn, ctx);
+  };
+  /* * * *  Misc  * * * */
+  /**
+   * @returns {Array<Array>} Deep copy of the data rows (including all columns).
+   */
+  prototype.toJSON = function(){
+    return _.merge([], this.getData());
+  };
+  prototype.toString = function(){
+    var labels;
+    labels = this.labels.map(function(it){
+      return "'" + it + "'";
+    }).join(', ');
+    return (this.constructor.name || this.constructor.displayName) + "(" + labels + ")";
+  };
+  return TimeSeriesData;
+}());
+module.exports = exports = TimeSeriesData;
+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/util/timeseries/timeseries.mod.js b/lib/util/timeseries/timeseries.mod.js
new file mode 100644 (file)
index 0000000..ca17718
--- /dev/null
@@ -0,0 +1,203 @@
+require.define('/node_modules/kraken/util/timeseries/timeseries.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var TimeSeriesData, exports, _;
+_ = require('kraken/util/underscore');
+/**
+ * @class Represents a collection of data columns aligned along a common timeline.
+ */
+TimeSeriesData = (function(){
+  TimeSeriesData.displayName = 'TimeSeriesData';
+  var prototype = TimeSeriesData.prototype, constructor = TimeSeriesData;
+  prototype.DEFAULT_OPTIONS = {};
+  prototype.options = {};
+  prototype.labels = [];
+  prototype.types = [];
+  prototype.untransformedRows = null;
+  prototype.rows = null;
+  prototype.columns = null;
+  prototype.dateColumn = null;
+  prototype.dataColumns = null;
+  /**
+   * @constructor
+   */;
+  function TimeSeriesData(data, opts){
+    var that, _ref;
+    if (!(typeof data === 'string' || _.isArray(data))) {
+      _ref = [data, null], opts = _ref[0], data = _ref[1];
+    }
+    this.options = __import(_.clone(this.DEFAULT_OPTIONS), opts || {});
+    this.transforms = [];
+    this.labels = this.options.labels || [];
+    this.types = this.options.types || [];
+    if (that = data || this.options.data) {
+      this.parseData(that);
+    }
+    this.rebuildDerived();
+  }
+  /* * * *  TimeSeriesData interface  * * * */
+  /**
+   * @returns {Array<Array>} List of rows, each of which includes all columns.
+   */
+  prototype.getData = function(){
+    return this.data;
+  };
+  /**
+   * @returns {Array<Array>} List of all columns (including date column).
+   */
+  prototype.getColumns = function(){
+    return this.columns;
+  };
+  /**
+   * @returns {Array<Date>} The date column.
+   */
+  prototype.getDateColumn = function(){
+    return this.dateColumn;
+  };
+  /**
+   * @returns {Array<Array>} List of all columns except the date column.
+   */
+  prototype.getDataColumns = function(){
+    return this.dataColumns;
+  };
+  /**
+   * @returns {Array<String>} List of column labels.
+   */
+  prototype.getLabels = function(){
+    return this.labels;
+  };
+  /* * * *  Parsing  * * * */
+  /**
+   * Subclass and override to perform preprocessing of the data.
+   * @private
+   */
+  prototype.parseData = function(rawData){
+    return this;
+  };
+  /**
+   * Rebuilds the row-oriented data matrix from the columns.
+   * @private
+   */
+  prototype.rebuildData = function(){
+    this.rows = _.zip.apply(_, this.columns);
+    return this.rebuildDerived();
+  };
+  /**
+   * Rebuilds the column-oriented data matrix from the columns.
+   * @private
+   */
+  prototype.rebuildColumns = function(){
+    this.columns = _.zip.apply(_, this.rows);
+    return this.rebuildDerived();
+  };
+  /**
+   * @private
+   */
+  prototype.rebuildDerived = function(){
+    while (this.transforms.length < this.columns.length) {
+      this.transforms.push([]);
+    }
+    this.dateColumn = this.columns[0];
+    this.dataColumns = this.columns.slice(1);
+    return this;
+  };
+  /* * * *  Data Transformation  * * * */
+  /**
+   * Applies the stack of transforms to the data.
+   * 
+   * TODO: Apply transforms in @getData()?
+   * @private
+   * @returns {this}
+   */
+  prototype.applyTransforms = function(){
+    var idx, fns, fn, ctx, _ref, _len, _i, _len2, _ref2;
+    for (idx = 0, _len = (_ref = this.transforms).length; idx < _len; ++idx) {
+      fns = _ref[idx];
+      for (_i = 0, _len2 = fns.length; _i < _len2; ++_i) {
+        _ref2 = fns[_i], fn = _ref2[0], ctx = _ref2[1];
+        (_ref2 = this.columns)[idx] = _ref2[idx].map(fn, ctx);
+      }
+    }
+    return this.rebuildData();
+  };
+  /**
+   * Clears all transforms and restores the original data.
+   * @returns {this}
+   */
+  prototype.clearTransforms = function(){
+    this.transforms = [];
+    this.rows = _.merge([], this.untransformedRows);
+    return this.rebuildColumns();
+  };
+  /**
+   * Add a data transform to the specified columns. The function is
+   * applied one-by-one (in column-major order), replacing the data
+   * with the mapped result.
+   * 
+   * @param {Number|Array} indices List one or more column indices to map. Negative
+   *  numbers are offset from the end of the columns list.
+   * @param {Function} fn Mapping function of the form:
+   *  `(single_value, row_idx, column) -> new_value`
+   * @param {Object} [ctx=this] Execution context for the function.
+   * @returns {this}
+   */
+  prototype.addTransform = function(indices, fn, ctx){
+    var num_cols, idx, _ref, _i, _len;
+    ctx == null && (ctx = this);
+    num_cols = this.columns.length;
+    if (typeof idx === 'function') {
+      _ref = [fn, indices, null], ctx = _ref[0], fn = _ref[1], indices = _ref[2];
+    }
+    if (indices == null) {
+      indices = _.range(num_cols);
+    }
+    if (!_.isArray(indices)) {
+      indices = [indices];
+    }
+    for (_i = 0, _len = indices.length; _i < _len; ++_i) {
+      idx = indices[_i];
+      idx %= num_cols;
+      if (idx < 0) {
+        idx += num_cols;
+      }
+      this.transforms[idx].push([fn, ctx]);
+    }
+    return this.applyTransforms();
+  };
+  /**
+   * Add a data transform to all columns except the date column. The function
+   * is applied one-by-one (in column-major order), replacing the data
+   * with the mapped result.
+   * 
+   * @param {Function} fn Mapping function of the form:
+   *  `(single_value, row_idx, column) -> new_value`
+   * @param {Object} [ctx=this] Execution context for the function.
+   * @returns {this}
+   */
+  prototype.addDataTransform = function(fn, ctx){
+    ctx == null && (ctx = this);
+    return this.addTransform(_.range(1, this.columns.length), fn, ctx);
+  };
+  /* * * *  Misc  * * * */
+  /**
+   * @returns {Array<Array>} Deep copy of the data rows (including all columns).
+   */
+  prototype.toJSON = function(){
+    return _.merge([], this.getData());
+  };
+  prototype.toString = function(){
+    var labels;
+    labels = this.labels.map(function(it){
+      return "'" + it + "'";
+    }).join(', ');
+    return (this.constructor.name || this.constructor.displayName) + "(" + labels + ")";
+  };
+  return TimeSeriesData;
+}());
+module.exports = exports = TimeSeriesData;
+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/util/underscore/array.js b/lib/util/underscore/array.js
new file mode 100644 (file)
index 0000000..ad3ba7f
--- /dev/null
@@ -0,0 +1,58 @@
+var I, defined, _, _array;
+_ = require('underscore');
+I = function(it){
+  return it;
+};
+defined = function(o){
+  return o != null;
+};
+_array = {
+  /**
+   * Transforms an Array of tuples (two-element Arrays) into an Object, such that for each
+   * tuple [k, v]:
+   *      result[k] = v if filter(v)
+   * @param {Array} o A collection.
+   * @param {Function} [filter=defined] Optional filter function. If omitted, will 
+   *  exclude `undefined` and `null` values.
+   * @return {Object} Transformed result.
+   */
+  generate: function(o, filter){
+    filter == null && (filter = defined);
+    return _.reduce(o, function(acc, _arg, idx){
+      var k, v;
+      k = _arg[0], v = _arg[1];
+      if (k && (!filter || filter(v, k))) {
+        acc[k] = v;
+      }
+      return acc;
+    }, {});
+  }
+  /**
+   * As {@link _.generate}, but first transforms the collection using `fn`.
+   * @param {Array} o A collection.
+   * @param {Function} [fn=I] Transformation function. Defaults to the identity transform.
+   * @param {Function} [filter=defined] Optional filter function. If omitted, will 
+   *  exclude `undefined` and `null` values.
+   * @param {Object} [context=o] Function context.
+   * @return {Object} Transformed result.
+   */,
+  synthesize: function(o, fn, filter, context){
+    fn == null && (fn = I);
+    filter == null && (filter = defined);
+    return _array.generate(_.map(o, fn, context), filter);
+  }
+  /**
+   * Symmetric Difference
+   */,
+  xor: function(a, b){
+    a = _.values(a);
+    b = _.values(b);
+    return _.union(_.difference(a, b), _.difference(b, a));
+  }
+};
+__import(exports, _array);
+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/util/underscore/array.mod.js b/lib/util/underscore/array.mod.js
new file mode 100644 (file)
index 0000000..4d3740a
--- /dev/null
@@ -0,0 +1,62 @@
+require.define('/node_modules/kraken/util/underscore/array.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var I, defined, _, _array;
+_ = require('underscore');
+I = function(it){
+  return it;
+};
+defined = function(o){
+  return o != null;
+};
+_array = {
+  /**
+   * Transforms an Array of tuples (two-element Arrays) into an Object, such that for each
+   * tuple [k, v]:
+   *      result[k] = v if filter(v)
+   * @param {Array} o A collection.
+   * @param {Function} [filter=defined] Optional filter function. If omitted, will 
+   *  exclude `undefined` and `null` values.
+   * @return {Object} Transformed result.
+   */
+  generate: function(o, filter){
+    filter == null && (filter = defined);
+    return _.reduce(o, function(acc, _arg, idx){
+      var k, v;
+      k = _arg[0], v = _arg[1];
+      if (k && (!filter || filter(v, k))) {
+        acc[k] = v;
+      }
+      return acc;
+    }, {});
+  }
+  /**
+   * As {@link _.generate}, but first transforms the collection using `fn`.
+   * @param {Array} o A collection.
+   * @param {Function} [fn=I] Transformation function. Defaults to the identity transform.
+   * @param {Function} [filter=defined] Optional filter function. If omitted, will 
+   *  exclude `undefined` and `null` values.
+   * @param {Object} [context=o] Function context.
+   * @return {Object} Transformed result.
+   */,
+  synthesize: function(o, fn, filter, context){
+    fn == null && (fn = I);
+    filter == null && (filter = defined);
+    return _array.generate(_.map(o, fn, context), filter);
+  }
+  /**
+   * Symmetric Difference
+   */,
+  xor: function(a, b){
+    a = _.values(a);
+    b = _.values(b);
+    return _.union(_.difference(a, b), _.difference(b, a));
+  }
+};
+__import(exports, _array);
+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/util/underscore/class.js b/lib/util/underscore/class.js
new file mode 100644 (file)
index 0000000..a9a3330
--- /dev/null
@@ -0,0 +1,73 @@
+var _, _cls;
+_ = require('underscore');
+_cls = {
+  /**
+   * @returns {Array<Class>} The list of all superclasses for this class
+   *  or object. Typically does not include Object or Function due to
+   *  the prototype's constructor being set by the subclass.
+   */
+  getSuperClasses: (function(){
+    function getSuperClasses(Cls){
+      var that, superclass, _ref;
+      if (!Cls) {
+        return [];
+      }
+      if (that = Cls.__superclass__ || Cls.superclass || ((_ref = Cls.__super__) != null ? _ref.constructor : void 8)) {
+        if (that !== Cls) {
+          superclass = that;
+        }
+      }
+      if (!superclass) {
+        if (typeof Cls !== 'function') {
+          Cls = Cls.constructor;
+        }
+        if (that = Cls.__superclass__ || Cls.superclass || ((_ref = Cls.__super__) != null ? _ref.constructor : void 8)) {
+          if (that !== Cls) {
+            superclass = that;
+          }
+        }
+      }
+      if (!superclass) {
+        return [];
+      } else {
+        return [superclass].concat(getSuperClasses(superclass));
+      }
+    }
+    return getSuperClasses;
+  }())
+  /**
+   * Looks up an attribute on the prototype of each class in the class
+   * hierarchy. Values from Object or Function are not typically included --
+   * see the note at `getSuperClasses()`.
+   * 
+   * @param {Object} obj Object on which to reflect.
+   * @param {String} prop Property to nab.
+   * @returns {Array} List of the values, from closest parent to furthest.
+   */,
+  pluckSuper: function(obj, prop){
+    if (!obj) {
+      return [];
+    }
+    return _(_cls.getSuperClasses(obj)).chain().pluck('prototype').pluck(prop).value();
+  }
+  /**
+   * As `.pluckSuper()` but includes value of `prop` on passed `obj`. Values
+   *  from Object or Function are not typically included -- see the note
+   *  at `getSuperClasses()`.
+   * 
+   * @returns {Array} List of the values, starting with the object's own
+   *  value, and then moving from closest parent to furthest.
+   */,
+  pluckSuperAndSelf: function(obj, prop){
+    if (!obj) {
+      return [];
+    }
+    return [obj[prop]].concat(_cls.pluckSuper(obj, prop));
+  }
+};
+__import(exports, _cls);
+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/util/underscore/class.mod.js b/lib/util/underscore/class.mod.js
new file mode 100644 (file)
index 0000000..4a00701
--- /dev/null
@@ -0,0 +1,77 @@
+require.define('/node_modules/kraken/util/underscore/class.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var _, _cls;
+_ = require('underscore');
+_cls = {
+  /**
+   * @returns {Array<Class>} The list of all superclasses for this class
+   *  or object. Typically does not include Object or Function due to
+   *  the prototype's constructor being set by the subclass.
+   */
+  getSuperClasses: (function(){
+    function getSuperClasses(Cls){
+      var that, superclass, _ref;
+      if (!Cls) {
+        return [];
+      }
+      if (that = Cls.__superclass__ || Cls.superclass || ((_ref = Cls.__super__) != null ? _ref.constructor : void 8)) {
+        if (that !== Cls) {
+          superclass = that;
+        }
+      }
+      if (!superclass) {
+        if (typeof Cls !== 'function') {
+          Cls = Cls.constructor;
+        }
+        if (that = Cls.__superclass__ || Cls.superclass || ((_ref = Cls.__super__) != null ? _ref.constructor : void 8)) {
+          if (that !== Cls) {
+            superclass = that;
+          }
+        }
+      }
+      if (!superclass) {
+        return [];
+      } else {
+        return [superclass].concat(getSuperClasses(superclass));
+      }
+    }
+    return getSuperClasses;
+  }())
+  /**
+   * Looks up an attribute on the prototype of each class in the class
+   * hierarchy. Values from Object or Function are not typically included --
+   * see the note at `getSuperClasses()`.
+   * 
+   * @param {Object} obj Object on which to reflect.
+   * @param {String} prop Property to nab.
+   * @returns {Array} List of the values, from closest parent to furthest.
+   */,
+  pluckSuper: function(obj, prop){
+    if (!obj) {
+      return [];
+    }
+    return _(_cls.getSuperClasses(obj)).chain().pluck('prototype').pluck(prop).value();
+  }
+  /**
+   * As `.pluckSuper()` but includes value of `prop` on passed `obj`. Values
+   *  from Object or Function are not typically included -- see the note
+   *  at `getSuperClasses()`.
+   * 
+   * @returns {Array} List of the values, starting with the object's own
+   *  value, and then moving from closest parent to furthest.
+   */,
+  pluckSuperAndSelf: function(obj, prop){
+    if (!obj) {
+      return [];
+    }
+    return [obj[prop]].concat(_cls.pluckSuper(obj, prop));
+  }
+};
+__import(exports, _cls);
+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/util/underscore/function.js b/lib/util/underscore/function.js
new file mode 100644 (file)
index 0000000..55a3a94
--- /dev/null
@@ -0,0 +1,34 @@
+var _, _fn, __slice = [].slice;
+_ = require('underscore');
+_fn = {
+  /**
+   * Decorates a function so that its receiver (`this`) is always added as the
+   * first argument, followed by the call arguments.
+   * @returns {Function}
+   */
+  methodize: function(fn){
+    var m, g, that;
+    m = fn.__methodized__;
+    if (m) {
+      return m;
+    }
+    g = fn.__genericized__;
+    if (that = g != null ? g.__wraps__ : void 8) {
+      return that;
+    }
+    m = fn.__methodized__ = function(){
+      var args;
+      args = __slice.call(arguments);
+      args.unshift(this);
+      return fn.apply(this, args);
+    };
+    m.__wraps__ = fn;
+    return m;
+  }
+};
+__import(exports, _fn);
+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/util/underscore/function.mod.js b/lib/util/underscore/function.mod.js
new file mode 100644 (file)
index 0000000..04e7948
--- /dev/null
@@ -0,0 +1,38 @@
+require.define('/node_modules/kraken/util/underscore/function.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var _, _fn, __slice = [].slice;
+_ = require('underscore');
+_fn = {
+  /**
+   * Decorates a function so that its receiver (`this`) is always added as the
+   * first argument, followed by the call arguments.
+   * @returns {Function}
+   */
+  methodize: function(fn){
+    var m, g, that;
+    m = fn.__methodized__;
+    if (m) {
+      return m;
+    }
+    g = fn.__genericized__;
+    if (that = g != null ? g.__wraps__ : void 8) {
+      return that;
+    }
+    m = fn.__methodized__ = function(){
+      var args;
+      args = __slice.call(arguments);
+      args.unshift(this);
+      return fn.apply(this, args);
+    };
+    m.__wraps__ = fn;
+    return m;
+  }
+};
+__import(exports, _fn);
+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/util/underscore/index.js b/lib/util/underscore/index.js
new file mode 100644 (file)
index 0000000..a0f8fd3
--- /dev/null
@@ -0,0 +1,31 @@
+var exports, _;
+_ = require('underscore');
+_.str = require('underscore.string');
+_.mixin(_.str.exports());
+_.mixin(require('kraken/util/underscore/function'));
+_.mixin(require('kraken/util/underscore/array'));
+_.mixin(require('kraken/util/underscore/object'));
+_.mixin(require('kraken/util/underscore/class'));
+_.mixin(require('kraken/util/underscore/kv'));
+_.mixin(require('kraken/util/underscore/string'));
+_.dump = function(o, label, expanded){
+  var k, v;
+  label == null && (label = 'dump');
+  expanded == null && (expanded = true);
+  if (!_.isArray(o) && _.isObject(o)) {
+    if (expanded) {
+      console.group(label);
+    } else {
+      console.groupCollapsed(label);
+    }
+    for (k in o) {
+      v = o[k];
+      console.log(k + ":", v);
+    }
+    console.groupEnd();
+  } else {
+    console.log(label, o);
+  }
+  return o;
+};
+module.exports = exports = _;
\ No newline at end of file
diff --git a/lib/util/underscore/index.mod.js b/lib/util/underscore/index.mod.js
new file mode 100644 (file)
index 0000000..e5fac00
--- /dev/null
@@ -0,0 +1,35 @@
+require.define('/node_modules/kraken/util/underscore/index.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var exports, _;
+_ = require('underscore');
+_.str = require('underscore.string');
+_.mixin(_.str.exports());
+_.mixin(require('kraken/util/underscore/function'));
+_.mixin(require('kraken/util/underscore/array'));
+_.mixin(require('kraken/util/underscore/object'));
+_.mixin(require('kraken/util/underscore/class'));
+_.mixin(require('kraken/util/underscore/kv'));
+_.mixin(require('kraken/util/underscore/string'));
+_.dump = function(o, label, expanded){
+  var k, v;
+  label == null && (label = 'dump');
+  expanded == null && (expanded = true);
+  if (!_.isArray(o) && _.isObject(o)) {
+    if (expanded) {
+      console.group(label);
+    } else {
+      console.groupCollapsed(label);
+    }
+    for (k in o) {
+      v = o[k];
+      console.log(k + ":", v);
+    }
+    console.groupEnd();
+  } else {
+    console.log(label, o);
+  }
+  return o;
+};
+module.exports = exports = _;
+
+});
diff --git a/lib/util/underscore/kv.js b/lib/util/underscore/kv.js
new file mode 100644 (file)
index 0000000..c722ba8
--- /dev/null
@@ -0,0 +1,74 @@
+var _, _kv;
+_ = require('underscore');
+_kv = {
+  /**
+   * Transforms an object to a string of URL-encoded KV-pairs (aka "www-form-encoding").
+   */
+  toKV: function(o, item_delim, kv_delim){
+    item_delim == null && (item_delim = '&');
+    kv_delim == null && (kv_delim = '=');
+    return _.reduce(o, function(acc, v, k){
+      if (k) {
+        acc.push(encodeURIComponent(k) + kv_delim + encodeURIComponent(v));
+      }
+      return acc;
+    }, []).join(item_delim);
+  }
+  /**
+   * Restores an object from a string of URL-encoded KV-pairs (aka "www-form-encoding").
+   */,
+  fromKV: function(qs, item_delim, kv_delim){
+    item_delim == null && (item_delim = '&');
+    kv_delim == null && (kv_delim = '=');
+    return _.reduce(qs.split(item_delim), function(acc, pair){
+      var idx, k, v, _ref;
+      idx = pair.indexOf(kv_delim);
+      if (idx !== -1) {
+        _ref = [pair.slice(0, idx), pair.slice(idx + 1)], k = _ref[0], v = _ref[1];
+      } else {
+        _ref = [pair, ''], k = _ref[0], v = _ref[1];
+      }
+      if (k) {
+        acc[decodeURIComponent(k)] = decodeURIComponent(v);
+      }
+      return acc;
+    }, {});
+  }
+  /**
+   * Copies and flattens a tree of sub-objects into namespaced keys on the parent object, such 
+   * that `{ "foo":{ "bar":1 } }` becomes `{ "foo.bar":1 }`.
+   */,
+  collapseObject: function(obj, parent, prefix){
+    parent == null && (parent = {});
+    prefix == null && (prefix = '');
+    if (prefix) {
+      prefix += '.';
+    }
+    _.each(obj, function(v, k){
+      if (_.isPlainObject(v)) {
+        return _.collapseObject(v, parent, prefix + k);
+      } else {
+        return parent[prefix + k] = v;
+      }
+    });
+    return parent;
+  }
+  /**
+   * Inverse of `.collapseObject()` -- copies and expands any dot-namespaced keys in the object, such
+   * that `{ "foo.bar":1 }` becomes `{ "foo":{ "bar":1 }}`.
+   */,
+  uncollapseObject: function(obj){
+    return _.reduce(obj, function(acc, v, k){
+      _.setNested(acc, k, v, {
+        ensure: true
+      });
+      return acc;
+    }, {});
+  }
+};
+__import(exports, _kv);
+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/util/underscore/kv.mod.js b/lib/util/underscore/kv.mod.js
new file mode 100644 (file)
index 0000000..ec5e1ff
--- /dev/null
@@ -0,0 +1,78 @@
+require.define('/node_modules/kraken/util/underscore/kv.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var _, _kv;
+_ = require('underscore');
+_kv = {
+  /**
+   * Transforms an object to a string of URL-encoded KV-pairs (aka "www-form-encoding").
+   */
+  toKV: function(o, item_delim, kv_delim){
+    item_delim == null && (item_delim = '&');
+    kv_delim == null && (kv_delim = '=');
+    return _.reduce(o, function(acc, v, k){
+      if (k) {
+        acc.push(encodeURIComponent(k) + kv_delim + encodeURIComponent(v));
+      }
+      return acc;
+    }, []).join(item_delim);
+  }
+  /**
+   * Restores an object from a string of URL-encoded KV-pairs (aka "www-form-encoding").
+   */,
+  fromKV: function(qs, item_delim, kv_delim){
+    item_delim == null && (item_delim = '&');
+    kv_delim == null && (kv_delim = '=');
+    return _.reduce(qs.split(item_delim), function(acc, pair){
+      var idx, k, v, _ref;
+      idx = pair.indexOf(kv_delim);
+      if (idx !== -1) {
+        _ref = [pair.slice(0, idx), pair.slice(idx + 1)], k = _ref[0], v = _ref[1];
+      } else {
+        _ref = [pair, ''], k = _ref[0], v = _ref[1];
+      }
+      if (k) {
+        acc[decodeURIComponent(k)] = decodeURIComponent(v);
+      }
+      return acc;
+    }, {});
+  }
+  /**
+   * Copies and flattens a tree of sub-objects into namespaced keys on the parent object, such 
+   * that `{ "foo":{ "bar":1 } }` becomes `{ "foo.bar":1 }`.
+   */,
+  collapseObject: function(obj, parent, prefix){
+    parent == null && (parent = {});
+    prefix == null && (prefix = '');
+    if (prefix) {
+      prefix += '.';
+    }
+    _.each(obj, function(v, k){
+      if (_.isPlainObject(v)) {
+        return _.collapseObject(v, parent, prefix + k);
+      } else {
+        return parent[prefix + k] = v;
+      }
+    });
+    return parent;
+  }
+  /**
+   * Inverse of `.collapseObject()` -- copies and expands any dot-namespaced keys in the object, such
+   * that `{ "foo.bar":1 }` becomes `{ "foo":{ "bar":1 }}`.
+   */,
+  uncollapseObject: function(obj){
+    return _.reduce(obj, function(acc, v, k){
+      _.setNested(acc, k, v, {
+        ensure: true
+      });
+      return acc;
+    }, {});
+  }
+};
+__import(exports, _kv);
+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/util/underscore/object.js b/lib/util/underscore/object.js
new file mode 100644 (file)
index 0000000..84d8087
--- /dev/null
@@ -0,0 +1,301 @@
+var getProto, OBJ_PROTO, hasOwn, objToString, DEFAULT_DELEGATE_OPTIONS, TOMBSTONE, DEFAULT_NESTED_OPTIONS, _, _ref, _obj, __slice = [].slice;
+_ = require('underscore');
+getProto = Object.getPrototypeOf;
+OBJ_PROTO = Object.prototype;
+_ref = {}, hasOwn = _ref.hasOwnProperty, objToString = _ref.toString;
+/**
+ * Default options for delegate-accessor functions.
+ */
+DEFAULT_DELEGATE_OPTIONS = exports.DEFAULT_DELEGATE_OPTIONS = {
+  getter: 'get',
+  setter: 'set',
+  deleter: 'unset'
+};
+/**
+ * Tombstone for deleted, non-passthrough keys.
+ */
+TOMBSTONE = exports.TOMBSTONE = {};
+/**
+ * Default options for nested-accessor functions.
+ */
+DEFAULT_NESTED_OPTIONS = exports.DEFAULT_NESTED_OPTIONS = __import({
+  ensure: false,
+  tombstone: TOMBSTONE
+}, DEFAULT_DELEGATE_OPTIONS);
+/**
+ * @namespace Functions for working with objects and object graphs.
+ */
+_obj = {
+  /**
+   * @returns {Boolean} Whether value is a plain object or not.
+   */
+  isPlainObject: function(obj){
+    var key;
+    if (!obj || objToString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval) {
+      return false;
+    }
+    if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
+      return false;
+    }
+    for (key in obj) {}
+    return key === void 8 || hasOwn.call(obj, key);
+  }
+  /**
+   * In-place removal of a value from an Array or Object.
+   */,
+  remove: function(obj, v){
+    var values, idx, k, _i, _len;
+    values = [].slice.call(arguments, 1);
+    if (_.isArray(obj) || obj instanceof Array) {
+      for (_i = 0, _len = values.length; _i < _len; ++_i) {
+        v = values[_i];
+        idx = obj.indexOf(v);
+        if (idx !== -1) {
+          obj.splice(idx, 1);
+        }
+      }
+    } else {
+      for (k in obj) {
+        v = obj[k];
+        if (-1 !== values.indexOf(v)) {
+          delete obj[k];
+        }
+      }
+    }
+    return obj;
+  }
+  /**
+   * Converts the collection to a list of its items:
+   * - Objects become a list of `[key, value]` pairs.
+   * - Strings become a list of characters.
+   * - Arguments objects become an array.
+   * - Arrays are copied.
+   */,
+  items: function(obj){
+    if (_.isObject(obj) && !_.isArguments(obj)) {
+      return _.map(obj, function(v, k){
+        return [k, v];
+      });
+    } else {
+      return [].slice.call(obj);
+    }
+  },
+  isMember: function(obj, v){
+    var values, common;
+    values = _.unique([].slice.call(arguments, 1));
+    common = _.intersection(_.values(obj), values);
+    return _.isEqual(values, common);
+  },
+  get: function(obj, key, def, opts){
+    var getter;
+    if (obj == null) {
+      return;
+    }
+    getter = (opts != null ? opts.getter : void 8) || 'get';
+    if (typeof obj[getter] === 'function') {
+      return obj[getter](key, def, opts);
+    } else {
+      if (obj[key] !== void 8) {
+        return obj[key];
+      } else {
+        return def;
+      }
+    }
+  },
+  set: function(obj, key, value, opts){
+    var values, setter, _ref;
+    if (obj == null) {
+      return;
+    }
+    if (key != null && _.isObject(key)) {
+      _ref = [key, value], values = _ref[0], opts = _ref[1];
+    } else {
+      values = (_ref = {}, _ref[key + ""] = value, _ref);
+    }
+    setter = (opts != null ? opts.setter : void 8) || 'set';
+    if (typeof obj[setter] === 'function') {
+      for (key in values) {
+        value = values[key];
+        obj[setter](key, value, opts);
+      }
+    } else {
+      for (key in values) {
+        value = values[key];
+        obj[key] = value;
+      }
+    }
+    return obj;
+  },
+  unset: function(obj, key, opts){
+    var deleter, _ref;
+    if (obj == null) {
+      return;
+    }
+    deleter = (opts != null ? opts.deleter : void 8) || 'unset';
+    if (typeof obj[deleter] === 'function') {
+      return obj[deleter](key, opts);
+    } else {
+      return _ref = obj[key], delete obj[key], _ref;
+    }
+  }
+  /**
+   * Searches a heirarchical object for a given subkey specified in dotted-property syntax,
+   * respecting sub-object accessor-methods (e.g., 'get', 'set') if they exist.
+   * 
+   * @param {Object} base The object to serve as the root of the property-chain.
+   * @param {Array|String} chain The property-chain to lookup.
+   * @param {Object} [opts] Options:
+   * @param {Boolean} [opts.ensure=false] If true, intermediate keys that are `null` or
+   *  `undefined` will be filled in with a new empty object `{}`, ensuring the get will
+   *   return valid metadata.
+   * @param {String} [opts.getter="get"] Name of the sub-object getter method use if it exists.
+   * @param {String} [opts.setter="set"] Name of the sub-object setter method use if it exists.
+   * @param {String} [opts.deleter="unset"] Name of the sub-object deleter method use if it exists.
+   * @param {Object} [opts.tombstone=TOMBSTONE] Sentinel value to be interpreted as no-passthrough,
+   *  forcing the lookup to fail and return `undefined`. TODO: opts.returnTombstone
+   * @returns {undefined|Object} If found, the object is of the form 
+   *  `{ key: Qualified key name, obj: Parent object of key, val: Value at obj[key], opts: Options }`. 
+   *  Otherwise `undefined`.
+   */,
+  getNestedMeta: function(obj, chain, opts){
+    var len;
+    if (typeof chain === 'string') {
+      chain = chain.split('.');
+    }
+    len = chain.length - 1;
+    opts = __import(_.clone(DEFAULT_NESTED_OPTIONS), opts || {});
+    return _.reduce(chain, function(obj, key, idx){
+      var val;
+      if (obj == null) {
+        return;
+      }
+      val = _.get(obj, key, void 8, opts);
+      if (val === opts.tombstone) {
+        if (!ops.ensure) {
+          return;
+        }
+        val = void 8;
+      }
+      if (idx === len) {
+        return {
+          key: key,
+          val: val,
+          obj: obj,
+          opts: opts
+        };
+      }
+      if (val == null && opts.ensure) {
+        val = {};
+        _.set(obj, key, val, opts);
+      }
+      return val;
+    }, obj);
+  }
+  /**
+   * Searches a heirarchical object for a given subkey specified in dotted-property syntax.
+   * @param {Object} obj The object to serve as the root of the property-chain.
+   * @param {Array|String} chain The property-chain to lookup.
+   * @param {Any} [def=undefined] Value to return if lookup fails.
+   * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`.
+   * @returns {null|Object} If found, returns the value, and otherwise `default`.
+   */,
+  getNested: function(obj, chain, def, opts){
+    var meta;
+    meta = _.getNestedMeta(obj, chain, opts);
+    if ((meta != null ? meta.val : void 8) === void 8) {
+      return def;
+    }
+    return meta.val;
+  }
+  /**
+   * Searches a heirarchical object for a given subkey specified in
+   * dotted-property syntax, setting it with the provided value if found.
+   * @param {Object} obj The object to serve as the root of the property-chain.
+   * @param {Array|String} chain The property-chain to lookup.
+   * @param {Any} value The value to set.
+   * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`.
+   * @returns {undefined|Any} If found, returns the old value, and otherwise `undefined`.
+   */,
+  setNested: function(obj, chain, value, opts){
+    var meta, key, val;
+    if (!(meta = _.getNestedMeta(obj, chain, opts))) {
+      return;
+    }
+    obj = meta.obj, key = meta.key, val = meta.val, opts = meta.opts;
+    _.set(obj, key, value, opts);
+    return val;
+  }
+  /**
+   * Searches a heirarchical object for a potentially-nested key and removes it.
+   * 
+   * @param {Object} obj The root of the lookup chain.
+   * @param {String|Array<String>} chain The chain of property-keys to navigate.
+   *  Nested keys can be supplied as a dot-delimited string (e.g., `_.unsetNested(obj, 'user.name')`),
+   *  or an array of strings, allowing for keys with dots (eg.,
+   *  `_.unsetNested(obj, ['products', 'by_price', '0.99'])`).
+   * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`.
+   * @returns {undefined|Any} The old value if found; otherwise `undefined`.
+   */,
+  unsetNested: function(obj, chain, opts){
+    var meta, key, val;
+    if (!(meta = _.getNestedMeta(obj, chain, opts))) {
+      return;
+    }
+    obj = meta.obj, key = meta.key, val = meta.val, opts = meta.opts;
+    _.unset(obj, key, opts);
+    return val;
+  }
+  /**
+   * Recursively merges together any number of donor objects into the target object.
+   * Modified from `jQuery.extend()`.
+   * 
+   * @param {Object} target Target object of the merge.
+   * @param {Object} ...donors Donor objects.
+   * @returns {Object} 
+   */,
+  merge: function(target){
+    var donors, donor, _i, _len;
+    target == null && (target = {});
+    donors = __slice.call(arguments, 1);
+    if (!(typeof target === "object" || _.isFunction(target))) {
+      target = _.isArray(donors[0])
+        ? []
+        : {};
+    }
+    for (_i = 0, _len = donors.length; _i < _len; ++_i) {
+      donor = donors[_i];
+      if (donor == null) {
+        continue;
+      }
+      _.each(donor, _fn);
+    }
+    return target;
+    function _fn(value, key){
+      var current, valueIsArray;
+      current = target[key];
+      if (target === value) {
+        return;
+      }
+      if (value && (_.isPlainObject(value) || (valueIsArray = _.isArray(value)))) {
+        if (valueIsArray) {
+          if (!_.isArray(current)) {
+            current = [];
+          }
+        } else {
+          if (!(current && typeof current === 'object')) {
+            current = {};
+          }
+        }
+        return _.set(target, key, _.merge(current, value));
+      } else if (value !== void 8) {
+        return _.set(target, key, value);
+      }
+    }
+  }
+};
+__import(exports, _obj);
+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/util/underscore/object.mod.js b/lib/util/underscore/object.mod.js
new file mode 100644 (file)
index 0000000..cc21a2e
--- /dev/null
@@ -0,0 +1,305 @@
+require.define('/node_modules/kraken/util/underscore/object.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var getProto, OBJ_PROTO, hasOwn, objToString, DEFAULT_DELEGATE_OPTIONS, TOMBSTONE, DEFAULT_NESTED_OPTIONS, _, _ref, _obj, __slice = [].slice;
+_ = require('underscore');
+getProto = Object.getPrototypeOf;
+OBJ_PROTO = Object.prototype;
+_ref = {}, hasOwn = _ref.hasOwnProperty, objToString = _ref.toString;
+/**
+ * Default options for delegate-accessor functions.
+ */
+DEFAULT_DELEGATE_OPTIONS = exports.DEFAULT_DELEGATE_OPTIONS = {
+  getter: 'get',
+  setter: 'set',
+  deleter: 'unset'
+};
+/**
+ * Tombstone for deleted, non-passthrough keys.
+ */
+TOMBSTONE = exports.TOMBSTONE = {};
+/**
+ * Default options for nested-accessor functions.
+ */
+DEFAULT_NESTED_OPTIONS = exports.DEFAULT_NESTED_OPTIONS = __import({
+  ensure: false,
+  tombstone: TOMBSTONE
+}, DEFAULT_DELEGATE_OPTIONS);
+/**
+ * @namespace Functions for working with objects and object graphs.
+ */
+_obj = {
+  /**
+   * @returns {Boolean} Whether value is a plain object or not.
+   */
+  isPlainObject: function(obj){
+    var key;
+    if (!obj || objToString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval) {
+      return false;
+    }
+    if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
+      return false;
+    }
+    for (key in obj) {}
+    return key === void 8 || hasOwn.call(obj, key);
+  }
+  /**
+   * In-place removal of a value from an Array or Object.
+   */,
+  remove: function(obj, v){
+    var values, idx, k, _i, _len;
+    values = [].slice.call(arguments, 1);
+    if (_.isArray(obj) || obj instanceof Array) {
+      for (_i = 0, _len = values.length; _i < _len; ++_i) {
+        v = values[_i];
+        idx = obj.indexOf(v);
+        if (idx !== -1) {
+          obj.splice(idx, 1);
+        }
+      }
+    } else {
+      for (k in obj) {
+        v = obj[k];
+        if (-1 !== values.indexOf(v)) {
+          delete obj[k];
+        }
+      }
+    }
+    return obj;
+  }
+  /**
+   * Converts the collection to a list of its items:
+   * - Objects become a list of `[key, value]` pairs.
+   * - Strings become a list of characters.
+   * - Arguments objects become an array.
+   * - Arrays are copied.
+   */,
+  items: function(obj){
+    if (_.isObject(obj) && !_.isArguments(obj)) {
+      return _.map(obj, function(v, k){
+        return [k, v];
+      });
+    } else {
+      return [].slice.call(obj);
+    }
+  },
+  isMember: function(obj, v){
+    var values, common;
+    values = _.unique([].slice.call(arguments, 1));
+    common = _.intersection(_.values(obj), values);
+    return _.isEqual(values, common);
+  },
+  get: function(obj, key, def, opts){
+    var getter;
+    if (obj == null) {
+      return;
+    }
+    getter = (opts != null ? opts.getter : void 8) || 'get';
+    if (typeof obj[getter] === 'function') {
+      return obj[getter](key, def, opts);
+    } else {
+      if (obj[key] !== void 8) {
+        return obj[key];
+      } else {
+        return def;
+      }
+    }
+  },
+  set: function(obj, key, value, opts){
+    var values, setter, _ref;
+    if (obj == null) {
+      return;
+    }
+    if (key != null && _.isObject(key)) {
+      _ref = [key, value], values = _ref[0], opts = _ref[1];
+    } else {
+      values = (_ref = {}, _ref[key + ""] = value, _ref);
+    }
+    setter = (opts != null ? opts.setter : void 8) || 'set';
+    if (typeof obj[setter] === 'function') {
+      for (key in values) {
+        value = values[key];
+        obj[setter](key, value, opts);
+      }
+    } else {
+      for (key in values) {
+        value = values[key];
+        obj[key] = value;
+      }
+    }
+    return obj;
+  },
+  unset: function(obj, key, opts){
+    var deleter, _ref;
+    if (obj == null) {
+      return;
+    }
+    deleter = (opts != null ? opts.deleter : void 8) || 'unset';
+    if (typeof obj[deleter] === 'function') {
+      return obj[deleter](key, opts);
+    } else {
+      return _ref = obj[key], delete obj[key], _ref;
+    }
+  }
+  /**
+   * Searches a heirarchical object for a given subkey specified in dotted-property syntax,
+   * respecting sub-object accessor-methods (e.g., 'get', 'set') if they exist.
+   * 
+   * @param {Object} base The object to serve as the root of the property-chain.
+   * @param {Array|String} chain The property-chain to lookup.
+   * @param {Object} [opts] Options:
+   * @param {Boolean} [opts.ensure=false] If true, intermediate keys that are `null` or
+   *  `undefined` will be filled in with a new empty object `{}`, ensuring the get will
+   *   return valid metadata.
+   * @param {String} [opts.getter="get"] Name of the sub-object getter method use if it exists.
+   * @param {String} [opts.setter="set"] Name of the sub-object setter method use if it exists.
+   * @param {String} [opts.deleter="unset"] Name of the sub-object deleter method use if it exists.
+   * @param {Object} [opts.tombstone=TOMBSTONE] Sentinel value to be interpreted as no-passthrough,
+   *  forcing the lookup to fail and return `undefined`. TODO: opts.returnTombstone
+   * @returns {undefined|Object} If found, the object is of the form 
+   *  `{ key: Qualified key name, obj: Parent object of key, val: Value at obj[key], opts: Options }`. 
+   *  Otherwise `undefined`.
+   */,
+  getNestedMeta: function(obj, chain, opts){
+    var len;
+    if (typeof chain === 'string') {
+      chain = chain.split('.');
+    }
+    len = chain.length - 1;
+    opts = __import(_.clone(DEFAULT_NESTED_OPTIONS), opts || {});
+    return _.reduce(chain, function(obj, key, idx){
+      var val;
+      if (obj == null) {
+        return;
+      }
+      val = _.get(obj, key, void 8, opts);
+      if (val === opts.tombstone) {
+        if (!ops.ensure) {
+          return;
+        }
+        val = void 8;
+      }
+      if (idx === len) {
+        return {
+          key: key,
+          val: val,
+          obj: obj,
+          opts: opts
+        };
+      }
+      if (val == null && opts.ensure) {
+        val = {};
+        _.set(obj, key, val, opts);
+      }
+      return val;
+    }, obj);
+  }
+  /**
+   * Searches a heirarchical object for a given subkey specified in dotted-property syntax.
+   * @param {Object} obj The object to serve as the root of the property-chain.
+   * @param {Array|String} chain The property-chain to lookup.
+   * @param {Any} [def=undefined] Value to return if lookup fails.
+   * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`.
+   * @returns {null|Object} If found, returns the value, and otherwise `default`.
+   */,
+  getNested: function(obj, chain, def, opts){
+    var meta;
+    meta = _.getNestedMeta(obj, chain, opts);
+    if ((meta != null ? meta.val : void 8) === void 8) {
+      return def;
+    }
+    return meta.val;
+  }
+  /**
+   * Searches a heirarchical object for a given subkey specified in
+   * dotted-property syntax, setting it with the provided value if found.
+   * @param {Object} obj The object to serve as the root of the property-chain.
+   * @param {Array|String} chain The property-chain to lookup.
+   * @param {Any} value The value to set.
+   * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`.
+   * @returns {undefined|Any} If found, returns the old value, and otherwise `undefined`.
+   */,
+  setNested: function(obj, chain, value, opts){
+    var meta, key, val;
+    if (!(meta = _.getNestedMeta(obj, chain, opts))) {
+      return;
+    }
+    obj = meta.obj, key = meta.key, val = meta.val, opts = meta.opts;
+    _.set(obj, key, value, opts);
+    return val;
+  }
+  /**
+   * Searches a heirarchical object for a potentially-nested key and removes it.
+   * 
+   * @param {Object} obj The root of the lookup chain.
+   * @param {String|Array<String>} chain The chain of property-keys to navigate.
+   *  Nested keys can be supplied as a dot-delimited string (e.g., `_.unsetNested(obj, 'user.name')`),
+   *  or an array of strings, allowing for keys with dots (eg.,
+   *  `_.unsetNested(obj, ['products', 'by_price', '0.99'])`).
+   * @param {Object} [opts] Options to pass to `{@link #getNestedMeta}`.
+   * @returns {undefined|Any} The old value if found; otherwise `undefined`.
+   */,
+  unsetNested: function(obj, chain, opts){
+    var meta, key, val;
+    if (!(meta = _.getNestedMeta(obj, chain, opts))) {
+      return;
+    }
+    obj = meta.obj, key = meta.key, val = meta.val, opts = meta.opts;
+    _.unset(obj, key, opts);
+    return val;
+  }
+  /**
+   * Recursively merges together any number of donor objects into the target object.
+   * Modified from `jQuery.extend()`.
+   * 
+   * @param {Object} target Target object of the merge.
+   * @param {Object} ...donors Donor objects.
+   * @returns {Object} 
+   */,
+  merge: function(target){
+    var donors, donor, _i, _len;
+    target == null && (target = {});
+    donors = __slice.call(arguments, 1);
+    if (!(typeof target === "object" || _.isFunction(target))) {
+      target = _.isArray(donors[0])
+        ? []
+        : {};
+    }
+    for (_i = 0, _len = donors.length; _i < _len; ++_i) {
+      donor = donors[_i];
+      if (donor == null) {
+        continue;
+      }
+      _.each(donor, _fn);
+    }
+    return target;
+    function _fn(value, key){
+      var current, valueIsArray;
+      current = target[key];
+      if (target === value) {
+        return;
+      }
+      if (value && (_.isPlainObject(value) || (valueIsArray = _.isArray(value)))) {
+        if (valueIsArray) {
+          if (!_.isArray(current)) {
+            current = [];
+          }
+        } else {
+          if (!(current && typeof current === 'object')) {
+            current = {};
+          }
+        }
+        return _.set(target, key, _.merge(current, value));
+      } else if (value !== void 8) {
+        return _.set(target, key, value);
+      }
+    }
+  }
+};
+__import(exports, _obj);
+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/util/underscore/string.js b/lib/util/underscore/string.js
new file mode 100644 (file)
index 0000000..8f73625
--- /dev/null
@@ -0,0 +1,101 @@
+var _, _str, _string, __slice = [].slice;
+_ = require('underscore');
+_str = require('underscore.string');
+_string = {
+  /**
+   * As _.str.chop but from the right.
+   */
+  rchop: function(s, step){
+    var i, out;
+    s = String(s);
+    i = s.length;
+    step = Number(step);
+    out = [];
+    if (step <= 0) {
+      return [s];
+    }
+    while (i > 0) {
+      out.unshift(s.slice(Math.max(0, i - step), i));
+      i -= step;
+    }
+    return out;
+  },
+  drop: function(s){
+    var parts, starting, part, _i, _len;
+    parts = __slice.call(arguments, 1);
+    do {
+      starting = s;
+      for (_i = 0, _len = parts.length; _i < _len; ++_i) {
+        part = parts[_i];
+        if (_str.startsWith(s, part)) {
+          s = s.slice(part.length);
+        }
+        if (_str.endsWith(s, part)) {
+          s = s.slice(0, s.length - part.length);
+        }
+      }
+    } while (s && s !== starting);
+    return s;
+  },
+  ldrop: function(s){
+    var parts, starting, part, _i, _len;
+    parts = __slice.call(arguments, 1);
+    do {
+      starting = s;
+      for (_i = 0, _len = parts.length; _i < _len; ++_i) {
+        part = parts[_i];
+        if (_str.startsWith(s, part)) {
+          s = s.slice(part.length);
+        }
+      }
+    } while (s && s !== starting);
+    return s;
+  },
+  rdrop: function(s){
+    var parts, starting, part, _i, _len;
+    parts = __slice.call(arguments, 1);
+    do {
+      starting = s;
+      for (_i = 0, _len = parts.length; _i < _len; ++_i) {
+        part = parts[_i];
+        if (_str.endsWith(s, part)) {
+          s = s.slice(0, s.length - part.length);
+        }
+      }
+    } while (s && s !== starting);
+    return s;
+  },
+  domize: function(key, value){
+    key == null && (key = '');
+    value == null && (value = '');
+    key = _str.trim(_str.underscored(key), '_');
+    if (arguments.length <= 1) {
+      return arguments.callee.bind(this, key);
+    } else {
+      return key + "_" + _str.trim(_str.underscored(value), '_');
+    }
+  },
+  shortname: function(s){
+    var parts;
+    if (s.length <= 6) {
+      return s;
+    }
+    parts = _(s).chain().underscored().trim('_').value().replace(/_+/g, '_').split('_').map(function(it){
+      return _.capitalize(it.slice(0, 2));
+    });
+    if (parts.length === 1) {
+      return s;
+    }
+    return parts.shift().toLowerCase() + parts.join('');
+  }
+};
+__import(_string, {
+  dropLeft: _string.ldrop,
+  dropRight: _string.rdrop
+});
+__import(exports, _string);
+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/util/underscore/string.mod.js b/lib/util/underscore/string.mod.js
new file mode 100644 (file)
index 0000000..1a38f92
--- /dev/null
@@ -0,0 +1,105 @@
+require.define('/node_modules/kraken/util/underscore/string.js.js', function(require, module, exports, __dirname, __filename, undefined){
+
+var _, _str, _string, __slice = [].slice;
+_ = require('underscore');
+_str = require('underscore.string');
+_string = {
+  /**
+   * As _.str.chop but from the right.
+   */
+  rchop: function(s, step){
+    var i, out;
+    s = String(s);
+    i = s.length;
+    step = Number(step);
+    out = [];
+    if (step <= 0) {
+      return [s];
+    }
+    while (i > 0) {
+      out.unshift(s.slice(Math.max(0, i - step), i));
+      i -= step;
+    }
+    return out;
+  },
+  drop: function(s){
+    var parts, starting, part, _i, _len;
+    parts = __slice.call(arguments, 1);
+    do {
+      starting = s;
+      for (_i = 0, _len = parts.length; _i < _len; ++_i) {
+        part = parts[_i];
+        if (_str.startsWith(s, part)) {
+          s = s.slice(part.length);
+        }
+        if (_str.endsWith(s, part)) {
+          s = s.slice(0, s.length - part.length);
+        }
+      }
+    } while (s && s !== starting);
+    return s;
+  },
+  ldrop: function(s){
+    var parts, starting, part, _i, _len;
+    parts = __slice.call(arguments, 1);
+    do {
+      starting = s;
+      for (_i = 0, _len = parts.length; _i < _len; ++_i) {
+        part = parts[_i];
+        if (_str.startsWith(s, part)) {
+          s = s.slice(part.length);
+        }
+      }
+    } while (s && s !== starting);
+    return s;
+  },
+  rdrop: function(s){
+    var parts, starting, part, _i, _len;
+    parts = __slice.call(arguments, 1);
+    do {
+      starting = s;
+      for (_i = 0, _len = parts.length; _i < _len; ++_i) {
+        part = parts[_i];
+        if (_str.endsWith(s, part)) {
+          s = s.slice(0, s.length - part.length);
+        }
+      }
+    } while (s && s !== starting);
+    return s;
+  },
+  domize: function(key, value){
+    key == null && (key = '');
+    value == null && (value = '');
+    key = _str.trim(_str.underscored(key), '_');
+    if (arguments.length <= 1) {
+      return arguments.callee.bind(this, key);
+    } else {
+      return key + "_" + _str.trim(_str.underscored(value), '_');
+    }
+  },
+  shortname: function(s){
+    var parts;
+    if (s.length <= 6) {
+      return s;
+    }
+    parts = _(s).chain().underscored().trim('_').value().replace(/_+/g, '_').split('_').map(function(it){
+      return _.capitalize(it.slice(0, 2));
+    });
+    if (parts.length === 1) {
+      return s;
+    }
+    return parts.shift().toLowerCase() + parts.join('');
+  }
+};
+__import(_string, {
+  dropLeft: _string.ldrop,
+  dropRight: _string.rdrop
+});
+__import(exports, _string);
+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/www/css/bootstrap-variables.css b/www/css/bootstrap-variables.css
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/www/css/chart.css b/www/css/chart.css
new file mode 100644 (file)
index 0000000..64e33b0
--- /dev/null
@@ -0,0 +1,12 @@
+.viewport svg .line {
+  fill: none;
+  stroke: #000;
+  stroke-width: 3px;
+}
+.viewport svg .axis {
+  font-size: 11px;
+  color: #666;
+}
+.viewport svg .axis text {
+  fill: #666;
+}
diff --git a/www/css/colors.css b/www/css/colors.css
new file mode 100644 (file)
index 0000000..f0aeb91
--- /dev/null
@@ -0,0 +1,5 @@
+/** Color Helpers **/
+/** Project Colors **/
+/* spot colors */
+/* background colors */
+/* text & link colors */
diff --git a/www/css/dashboard.css b/www/css/dashboard.css
new file mode 100644 (file)
index 0000000..c01056a
--- /dev/null
@@ -0,0 +1,28 @@
+/* * * *  Subnav & Tabs  * * * {{{ */
+.graphs.tabbable .nav {
+  margin-bottom: 0;
+}
+.graphs.tabbable .nav li h3 {
+  line-height: 14px;
+  margin: 2px;
+  padding: 8px 12px;
+  border-radius: 5px;
+}
+.graphs.tabbable .nav li {
+  margin-right: 4px;
+}
+.graphs.tabbable .tab-pane {
+  padding: 0.5em;
+  margin-top: 18px;
+}
+/* }}} */
+#dashboard section.graph {
+  margin: 0 auto;
+  padding: 3.5em 0;
+}
+#dashboard section.graph:first-child {
+  margin-top: 0;
+}
+#dashboard .graph-links-row {
+  display: none;
+}
diff --git a/www/css/data.css b/www/css/data.css
new file mode 100644 (file)
index 0000000..01468e5
--- /dev/null
@@ -0,0 +1,328 @@
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
+section.graph section.data-ui {
+  height: 100%;
+  min-height: 500px;
+  zoom: 1;
+/* * * *  DataSet UI  * * * */
+/* * * *  Edit Metric UI * * * */
+/* * * *  DataSource UI  * * * */
+}
+section.graph section.data-ui:before,
+section.graph section.data-ui:after {
+  content: "";
+  display: table;
+}
+section.graph section.data-ui:after {
+  clear: both;
+}
+section.graph section.data-ui h4 {
+  display: none;
+}
+section.graph section.data-ui .data_set_view_pane {
+  position: absolute;
+  0: 0;
+  ((null)): 0;
+  width: 40%;
+  height: 100%;
+  min-height: 500px;
+}
+section.graph section.data-ui .metric_edit_view_pane {
+  float: right;
+  width: 60%;
+  min-height: 100%;
+}
+section.graph section.data-ui .dataset-controls {
+  padding: 1em;
+}
+section.graph section.data-ui section.dataset-ui {
+  height: 100%;
+  min-height: 500px;
+  border-right: 1px solid #ccc;
+}
+section.graph section.data-ui section.dataset-ui .inner {
+  padding: 1em 0 1em 1em;
+}
+section.graph section.data-ui section.dataset-ui .dataset-buttons {
+  margin-bottom: 1em;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics thead {
+  display: none;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics td {
+  line-height: 1;
+  vertical-align: middle;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics tr:last-child td {
+  border-bottom: 1px solid #ddd;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .col-label {
+  width: 40%;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .col-source {
+  width: 25%;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .col-time {
+  width: 25%;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .col-actions {
+  padding-right: 14px;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .col-source,
+section.graph section.data-ui section.dataset-ui .dataset-metrics .col-time {
+  font-size: 80%;
+  color: #333;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .dataset-metric {
+  cursor: pointer;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .dataset-metric.metric-active {
+  font-weight: bold;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .dataset-metric.metric-active td {
+  border: 1px solid #ccc;
+  border-width: 1px 0;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .dataset-metric.metric-active .activity-arrow {
+  display: block;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .activity-arrow {
+  display: none;
+  position: absolute;
+  top: -1px;
+  right: -1px;
+  width: 0;
+  height: 0;
+  border-top: 0.5em solid transparent;
+  border-right: 10px solid #ccc;
+  border-bottom: 0.5em solid transparent;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .activity-arrow .inner {
+  position: absolute;
+  top: -0.5em;
+  left: 1px;
+  width: 0;
+  height: 0;
+  padding: 0;
+  border-top: 0.5em solid transparent;
+  border-right: 10px solid #fff;
+  border-bottom: 0.5em solid transparent;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .delete-metric-button {
+  color: #333;
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics .delete-metric-button:hover {
+  color: #9d261d;
+  opacity: 1;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
+}
+section.graph section.data-ui section.dataset-ui .dataset-metrics-empty {
+  display: none;
+}
+section.graph section.data-ui section.metric-edit-ui {
+  display: none;
+  min-height: 100%;
+}
+section.graph section.data-ui section.metric-edit-ui .inner {
+  padding: 1em;
+}
+section.graph section.data-ui section.metric-edit-ui .metric-header {
+  min-height: 38px;
+  font-size: 18px;
+  line-height: 25px;
+}
+section.graph section.data-ui section.metric-edit-ui .metric-header .color-swatch {
+  position: absolute;
+  top: 3px;
+  right: 0;
+  width: 30px;
+  height: 30px;
+}
+section.graph section.data-ui section.metric-edit-ui .metric-header .color-swatch .add-on,
+section.graph section.data-ui section.metric-edit-ui .metric-header .color-swatch i {
+  display: block;
+  width: 100%;
+  height: 100%;
+  border: 0;
+  padding: 0;
+  outline: 0;
+}
+section.graph section.data-ui section.metric-edit-ui .metric-header .color-swatch i {
+  border: 1px solid #333;
+  border-color: #ddd #333 #333 #ddd;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+section.graph section.data-ui section.metric-edit-ui .metric-header input {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 38px;
+  width: auto;
+  font-size: 18px;
+  line-height: 25px;
+  height: 25px;
+  padding: 6px;
+}
+section.graph section.data-ui section.datasource-ui i {
+  z-index: 100;
+}
+section.graph section.data-ui section.datasource-ui .expand-datasource-ui-button {
+  display: block;
+}
+section.graph section.data-ui section.datasource-ui .collapse-datasource-ui-button {
+  display: none;
+}
+section.graph section.data-ui section.datasource-ui.in .expand-datasource-ui-button {
+  display: none;
+}
+section.graph section.data-ui section.datasource-ui.in .collapse-datasource-ui-button {
+  display: block;
+}
+section.graph section.data-ui section.datasource-ui .datasource-summary,
+section.graph section.data-ui section.datasource-ui .datasource-summary:hover {
+  cursor: pointer;
+}
+section.graph section.data-ui section.datasource-ui .datasource-summary i {
+  position: absolute;
+  top: 50%;
+  right: 14px;
+  margin-top: -7px;
+}
+section.graph section.data-ui section.datasource-ui .datasource-summary .breadcrumb {
+  margin-bottom: 0;
+  font-weight: bold;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-tabs {
+  top: -1px;
+  padding: 7px;
+  border: 1px solid #ddd;
+  border-top: 0;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector h1,
+section.graph section.data-ui section.datasource-ui .datasource-selector h2,
+section.graph section.data-ui section.datasource-ui .datasource-selector h3,
+section.graph section.data-ui section.datasource-ui .datasource-selector h4,
+section.graph section.data-ui section.datasource-ui .datasource-selector h5,
+section.graph section.data-ui section.datasource-ui .datasource-selector h6 {
+  margin: 0 -1px 3px 0;
+  padding: 8px 12px;
+  min-width: 74px;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-list {
+  height: 100%;
+  display: table-cell;
+  float: none;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-list li a {
+  min-width: 150px;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info {
+  display: table-cell;
+  padding: 1em;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .well {
+  padding: 0.75em;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .source-id,
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .source-format,
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .source-charttype,
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info thead,
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .source-metric-idx {
+  display: none;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .datasource-source-details {
+  font-size: 12px;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .source-name {
+  font-size: 15px;
+  margin-bottom: 0.75em;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .source-url {
+  display: block;
+  width: 95%;
+  font-family: menlo, monospace;
+  font-size: 11px;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .datasource-source-time {
+  margin: 0.5em;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .datasource-source-time > * {
+  display: inline-block;
+  margin-right: 1em;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .datasource-source-metric,
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .datasource-source-metric:hover {
+  cursor: pointer;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .datasource-source-metric.active td {
+  color: #3a87ad;
+  background-color: #d9edf7;
+  border: 1px solid #3a87ad;
+  border-width: 1px 0;
+}
+section.graph section.data-ui section.datasource-ui .datasource-selector .datasource-sources-info .datasource-source-metric .source-metric-type {
+  font-family: menlo, monospace;
+}
+section.graph section.data-ui .data-ui {
+  zoom: 1;
+}
+section.graph section.data-ui .data-ui:before,
+section.graph section.data-ui .data-ui:after {
+  content: "";
+  display: table;
+}
+section.graph section.data-ui .data-ui:after {
+  clear: both;
+}
diff --git a/www/css/docs.css b/www/css/docs.css
new file mode 100644 (file)
index 0000000..80816fe
--- /dev/null
@@ -0,0 +1,40 @@
+h1,
+h2,
+h3,
+h4,
+h5 {
+  font-weight: normal;
+  margin: 0;
+  margin-top: 1em;
+  padding: 0.5em 0;
+}
+h1.page-header {
+  padding-bottom: 0;
+}
+h2 {
+  font-size: 18px;
+}
+h3,
+h4 {
+  font-size: 13px;
+  text-transform: uppercase;
+  letter-spacing: 0.2em;
+  word-spacing: 0.1em;
+}
+#content {
+  height: 100%;
+}
+header,
+#doc {
+  margin: 0 auto;
+  width: 60%;
+  max-width: 800px;
+  min-width: 600px;
+}
+footer {
+  margin-top: 3em;
+  padding: 1em 3em 3em;
+  background-color: #fbfbfb;
+  border-top: 1px solid #ddd;
+  font-size: 11px;
+}
diff --git a/www/css/geo-display.css b/www/css/geo-display.css
new file mode 100644 (file)
index 0000000..5a0892e
--- /dev/null
@@ -0,0 +1,88 @@
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
+section.geo {
+  position: relative;
+  margin: 0 auto;
+  min-width: 640px;
+  max-width: 960px;
+}
+section.geo #worldmap {
+  position: relative;
+  min-width: 960px;
+  min-height: 500px;
+}
+section.geo .geo-spinner {
+  position: absolute;
+  z-index: 300;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+section.geo .help {
+  text-align: right;
+}
+section.geo #infobox {
+  display: none;
+  position: absolute;
+  pointer-events: none;
+  border: 1px solid #333;
+}
+section.geo svg {
+  background: #eee;
+  margin: 0 auto;
+}
+section.geo .frame {
+  stroke: #333;
+  fill: none;
+  pointer-events: all;
+}
+section.geo .feature {
+  stroke: #ccc;
+}
diff --git a/www/css/graph-display-print.css b/www/css/graph-display-print.css
new file mode 100644 (file)
index 0000000..93a3e88
--- /dev/null
@@ -0,0 +1,9 @@
+section.graph-display.graph {
+  page-break-after: always;
+}
+section.graph-display.graph .graph-legend {
+  position: relative;
+}
+section.graph-display.graph .graph-permalink {
+  padding: 1em;
+}
diff --git a/www/css/graph-display.css b/www/css/graph-display.css
new file mode 100644 (file)
index 0000000..2fc3bf1
--- /dev/null
@@ -0,0 +1,648 @@
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
+section.graph-display.graph {
+  position: relative;
+  max-width: 900px;
+  margin: 3em auto;
+/* * * *  Chart & Viewport  * * * {{{ */
+/* }}} */
+/* * * *  Subnav & Tabs  * * * {{{ */
+/* }}} */
+/* * * *  Graph Details  * * * {{{ */
+/* }}} */
+/* * * *  Chart Options  * * * {{{ */
+/* }}} End Chart Options */
+}
+section.graph-display.graph * {
+  position: relative;
+}
+section.graph-display.graph .graph-name-row {
+  margin-bottom: 1em;
+  zoom: 1;
+}
+section.graph-display.graph .graph-name-row:before,
+section.graph-display.graph .graph-name-row:after {
+  content: "";
+  display: table;
+}
+section.graph-display.graph .graph-name-row:after {
+  clear: both;
+}
+section.graph-display.graph .callout {
+  float: left;
+  z-index: 100;
+  width: 15.385em;
+  padding: 0 6em 0 0;
+  background-color: #fff;
+  text-align: right;
+}
+section.graph-display.graph .callout .latest-metric {
+  font-size: 18px;
+  line-height: 36px;
+}
+section.graph-display.graph .callout .metric-change {
+  font: 11px/1.5 "helvetica neue", helvetica, arial, sans-serif;
+}
+section.graph-display.graph .callout .metric-change span {
+  display: inline-block;
+}
+section.graph-display.graph .callout .metric-change span.dates {
+  width: 140px;
+}
+section.graph-display.graph .callout .metric-change span.value {
+  width: 60px;
+}
+section.graph-display.graph .callout .metric-change span.value.delta-positive {
+  color: #009048;
+}
+section.graph-display.graph .callout .metric-change span.value.delta-negative {
+  color: #c9313d;
+}
+section.graph-display.graph .graph-legend {
+  position: absolute;
+  top: 1em;
+  left: 0;
+  z-index: 100;
+  width: 14.667em;
+  padding: 1em;
+  background-color: rgba(255,255,255,0.75);
+  font: 12px/1.5 "helvetica neue", helvetica, arial, sans-serif;
+  border: 1px solid #eee;
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+section.graph-display.graph .graph-legend b {
+  display: inline-block;
+  width: 10em;
+}
+section.graph-display.graph .graph-legend .whole {
+  display: inline-block;
+  width: 2.5em;
+  text-align: right;
+}
+section.graph-display.graph .graph-legend .fraction {
+  text-align: left;
+}
+section.graph-display.graph .graph-legend .suffix {
+  text-align: left;
+}
+section.graph-display.graph .graph-viewport-row > .inner {
+  padding-left: 17.385em;
+}
+section.graph-display.graph .viewport:hover + .graph-legend {
+  border: 1px solid #eee;
+}
+section.graph-display.graph .viewport {
+  position: relative;
+  min-width: 200px;
+  min-height: 320px;
+  margin-bottom: 1.5em;
+  overflow: hidden;
+}
+section.graph-display.graph .graph-settings.tabbable .nav {
+  margin-bottom: 0;
+}
+section.graph-display.graph .graph-settings.tabbable .nav li h3 {
+  line-height: 14px;
+  margin: 2px;
+  padding: 8px 12px;
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+section.graph-display.graph .graph-settings.tabbable .nav li {
+  margin-right: 4px;
+}
+section.graph-display.graph .graph-settings.tabbable .tab-pane {
+  padding: 0.5em;
+  margin-top: 18px;
+}
+section.graph-display.graph .graph-settings.tabbable .tab-pane.graph-data-pane {
+  margin-top: 0;
+  padding: 0;
+}
+section.graph-display.graph .graph-controls {
+  z-index: 100;
+  margin-top: 2px;
+}
+section.graph-display.graph .graph-controls > .btn,
+section.graph-display.graph .graph-controls > .btn-group {
+  margin: 0 0.5em;
+}
+section.graph-display.graph .graph-controls .btn-group {
+  display: inline-block;
+}
+section.graph-display.graph .graph-controls input[type="button"] {
+  min-width: 5em;
+  text-align: center;
+}
+section.graph-display.graph .graph-permalink {
+  display: inline-block;
+}
+section.graph-display.graph .graph-permalink input {
+  cursor: auto;
+}
+section.graph-display.graph .ug {
+  padding-left: 3em;
+}
+section.graph-display.graph form.graph-details {
+  position: relative;
+}
+section.graph-display.graph form.graph-details .name-row {
+  font-size: 120%;
+  line-height: 2em;
+}
+section.graph-display.graph form.graph-details .name-row input.name {
+  font-size: 120%;
+  line-height: 1.2;
+  height: 1.2em;
+  border-color: #eee;
+  width: 98%;
+}
+section.graph-display.graph form.graph-details .row-fluid .half.control-group {
+  width: 50%;
+  float: left;
+  margin-left: 0;
+  margin-right: 0;
+}
+section.graph-display.graph form.graph-details .row-fluid label {
+  width: 100px;
+}
+section.graph-display.graph form.graph-details .row-fluid .controls {
+  margin-left: 110px;
+}
+section.graph-display.graph form.graph-details .help-block {
+  font-size: 11px;
+  line-height: 1.3;
+}
+section.graph-display.graph .chart-options fieldset {
+  border: 0px;
+}
+section.graph-display.graph .chart-option.field {
+  float: left;
+  z-index: 3;
+  padding: 0.5em;
+  margin: 0.4em;
+  min-width: 200px;
+  max-width: 250px;
+  min-height: 1.5em;
+  line-height: 1.5;
+  overflow: hidden;
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+  background-color: #ccc;
+  font-size: 90%;
+/* Category/Tag Colors {{{ */
+/* }}} */
+}
+section.graph-display.graph .chart-option.field h3 {
+  font-size: 14px;
+  line-height: 1.3;
+  cursor: pointer;
+}
+section.graph-display.graph .chart-option.field .close {
+  position: absolute;
+  top: 0;
+  right: 0.1em;
+  width: 1em;
+  height: 1em;
+  line-height: 1.2em;
+  text-align: center;
+  text-decoration: none;
+  z-index: 10;
+  cursor: pointer;
+  opacity: 0.3;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=30);
+}
+section.graph-display.graph .chart-option.field .close:hover {
+  opacity: 0.6;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60);
+}
+section.graph-display.graph .chart-option.field .shortname {
+  font-weight: bold;
+  color: #fff;
+  min-height: 1.5em;
+}
+section.graph-display.graph .chart-option.field .name {
+  display: none;
+  font-weight: bold;
+}
+section.graph-display.graph .chart-option.field input.value:not([type="checkbox"]) {
+  width: 240px;
+  font-family: menlo, monospace;
+}
+section.graph-display.graph .chart-option.field .type::before {
+  content: "Type: ";
+  font-weight: bold;
+}
+section.graph-display.graph .chart-option.field .default::before {
+  content: "Default: ";
+  font-weight: bold;
+}
+section.graph-display.graph .chart-option.field .desc {
+  position: relative;
+}
+section.graph-display.graph .chart-option.field .tags,
+section.graph-display.graph .chart-option.field .examples {
+  cursor: pointer;
+}
+section.graph-display.graph .chart-option.field .tags {
+  font-size: 85%;
+}
+section.graph-display.graph .chart-option.field .tags::before {
+  content: "Tags: ";
+  font-weight: bold;
+}
+section.graph-display.graph .chart-option.field .tags .tag {
+  margin: 0.2em;
+  line-height: 1.5;
+  padding: 0.2em;
+  white-space: nowrap;
+  color: #fff;
+  background-color: rgba(255,255,255,0.15);
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+section.graph-display.graph .chart-option.field .examples {
+  display: none;
+}
+section.graph-display.graph .chart-option.field .examples::before {
+  content: "Examples";
+  font-weight: bold;
+}
+section.graph-display.graph .chart-option.field .examples .example {
+  position: relative;
+}
+section.graph-display.graph .chart-option.field.collapsed {
+  z-index: 2;
+  width: auto;
+  min-width: 50px;
+  min-height: 2em;
+  max-width: none;
+  line-height: 2;
+  cursor: pointer;
+  text-align: center;
+}
+section.graph-display.graph .chart-option.field.collapsed * {
+  display: none;
+}
+section.graph-display.graph .chart-option.field.collapsed .shortname {
+  display: inline-block;
+  min-width: 50px;
+  min-height: auto;
+}
+section.graph-display.graph .chart-option.field.category_0 {
+  color: #eee;
+  background-color: #182b53;
+}
+section.graph-display.graph .chart-option.field.category_0 label,
+section.graph-display.graph .chart-option.field.category_0 h1,
+section.graph-display.graph .chart-option.field.category_0 h2,
+section.graph-display.graph .chart-option.field.category_0 h3,
+section.graph-display.graph .chart-option.field.category_0 .shortname,
+section.graph-display.graph .chart-option.field.category_0 .name,
+section.graph-display.graph .chart-option.field.category_0 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_0 {
+  color: #eee;
+  background-color: #182b53;
+}
+section.graph-display.graph .chart-option.field.category_1 {
+  color: #eee;
+  background-color: #0080ff;
+}
+section.graph-display.graph .chart-option.field.category_1 label,
+section.graph-display.graph .chart-option.field.category_1 h1,
+section.graph-display.graph .chart-option.field.category_1 h2,
+section.graph-display.graph .chart-option.field.category_1 h3,
+section.graph-display.graph .chart-option.field.category_1 .shortname,
+section.graph-display.graph .chart-option.field.category_1 .name,
+section.graph-display.graph .chart-option.field.category_1 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_1 {
+  color: #eee;
+  background-color: #0080ff;
+}
+section.graph-display.graph .chart-option.field.category_2 {
+  color: #333;
+  background-color: #a6d9ff;
+}
+section.graph-display.graph .chart-option.field.category_2 label,
+section.graph-display.graph .chart-option.field.category_2 h1,
+section.graph-display.graph .chart-option.field.category_2 h2,
+section.graph-display.graph .chart-option.field.category_2 h3,
+section.graph-display.graph .chart-option.field.category_2 .shortname,
+section.graph-display.graph .chart-option.field.category_2 .name,
+section.graph-display.graph .chart-option.field.category_2 .close {
+  color: #333;
+}
+section.graph-display.graph .chart-option.field .tag.category_2 {
+  color: #333;
+  background-color: #a6d9ff;
+}
+section.graph-display.graph .chart-option.field.category_3 {
+  color: #333;
+  background-color: #51e3c5;
+}
+section.graph-display.graph .chart-option.field.category_3 label,
+section.graph-display.graph .chart-option.field.category_3 h1,
+section.graph-display.graph .chart-option.field.category_3 h2,
+section.graph-display.graph .chart-option.field.category_3 h3,
+section.graph-display.graph .chart-option.field.category_3 .shortname,
+section.graph-display.graph .chart-option.field.category_3 .name,
+section.graph-display.graph .chart-option.field.category_3 .close {
+  color: #333;
+}
+section.graph-display.graph .chart-option.field .tag.category_3 {
+  color: #333;
+  background-color: #51e3c5;
+}
+section.graph-display.graph .chart-option.field.category_4 {
+  color: #eee;
+  background-color: #254a59;
+}
+section.graph-display.graph .chart-option.field.category_4 label,
+section.graph-display.graph .chart-option.field.category_4 h1,
+section.graph-display.graph .chart-option.field.category_4 h2,
+section.graph-display.graph .chart-option.field.category_4 h3,
+section.graph-display.graph .chart-option.field.category_4 .shortname,
+section.graph-display.graph .chart-option.field.category_4 .name,
+section.graph-display.graph .chart-option.field.category_4 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_4 {
+  color: #eee;
+  background-color: #254a59;
+}
+section.graph-display.graph .chart-option.field.category_5 {
+  color: #eee;
+  background-color: #32938a;
+}
+section.graph-display.graph .chart-option.field.category_5 label,
+section.graph-display.graph .chart-option.field.category_5 h1,
+section.graph-display.graph .chart-option.field.category_5 h2,
+section.graph-display.graph .chart-option.field.category_5 h3,
+section.graph-display.graph .chart-option.field.category_5 .shortname,
+section.graph-display.graph .chart-option.field.category_5 .name,
+section.graph-display.graph .chart-option.field.category_5 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_5 {
+  color: #eee;
+  background-color: #32938a;
+}
+section.graph-display.graph .chart-option.field.category_6 {
+  color: #eee;
+  background-color: #83bb32;
+}
+section.graph-display.graph .chart-option.field.category_6 label,
+section.graph-display.graph .chart-option.field.category_6 h1,
+section.graph-display.graph .chart-option.field.category_6 h2,
+section.graph-display.graph .chart-option.field.category_6 h3,
+section.graph-display.graph .chart-option.field.category_6 .shortname,
+section.graph-display.graph .chart-option.field.category_6 .name,
+section.graph-display.graph .chart-option.field.category_6 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_6 {
+  color: #eee;
+  background-color: #83bb32;
+}
+section.graph-display.graph .chart-option.field.category_7 {
+  color: #333;
+  background-color: #b1e43b;
+}
+section.graph-display.graph .chart-option.field.category_7 label,
+section.graph-display.graph .chart-option.field.category_7 h1,
+section.graph-display.graph .chart-option.field.category_7 h2,
+section.graph-display.graph .chart-option.field.category_7 h3,
+section.graph-display.graph .chart-option.field.category_7 .shortname,
+section.graph-display.graph .chart-option.field.category_7 .name,
+section.graph-display.graph .chart-option.field.category_7 .close {
+  color: #333;
+}
+section.graph-display.graph .chart-option.field .tag.category_7 {
+  color: #333;
+  background-color: #b1e43b;
+}
+section.graph-display.graph .chart-option.field.category_8 {
+  color: #333;
+  background-color: #f1d950;
+}
+section.graph-display.graph .chart-option.field.category_8 label,
+section.graph-display.graph .chart-option.field.category_8 h1,
+section.graph-display.graph .chart-option.field.category_8 h2,
+section.graph-display.graph .chart-option.field.category_8 h3,
+section.graph-display.graph .chart-option.field.category_8 .shortname,
+section.graph-display.graph .chart-option.field.category_8 .name,
+section.graph-display.graph .chart-option.field.category_8 .close {
+  color: #333;
+}
+section.graph-display.graph .chart-option.field .tag.category_8 {
+  color: #333;
+  background-color: #f1d950;
+}
+section.graph-display.graph .chart-option.field.category_9 {
+  color: #333;
+  background-color: #ef8158;
+}
+section.graph-display.graph .chart-option.field.category_9 label,
+section.graph-display.graph .chart-option.field.category_9 h1,
+section.graph-display.graph .chart-option.field.category_9 h2,
+section.graph-display.graph .chart-option.field.category_9 h3,
+section.graph-display.graph .chart-option.field.category_9 .shortname,
+section.graph-display.graph .chart-option.field.category_9 .name,
+section.graph-display.graph .chart-option.field.category_9 .close {
+  color: #333;
+}
+section.graph-display.graph .chart-option.field .tag.category_9 {
+  color: #333;
+  background-color: #ef8158;
+}
+section.graph-display.graph .chart-option.field.category_10 {
+  color: #eee;
+  background-color: #dc3522;
+}
+section.graph-display.graph .chart-option.field.category_10 label,
+section.graph-display.graph .chart-option.field.category_10 h1,
+section.graph-display.graph .chart-option.field.category_10 h2,
+section.graph-display.graph .chart-option.field.category_10 h3,
+section.graph-display.graph .chart-option.field.category_10 .shortname,
+section.graph-display.graph .chart-option.field.category_10 .name,
+section.graph-display.graph .chart-option.field.category_10 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_10 {
+  color: #eee;
+  background-color: #dc3522;
+}
+section.graph-display.graph .chart-option.field.category_11 {
+  color: #eee;
+  background-color: #c9313d;
+}
+section.graph-display.graph .chart-option.field.category_11 label,
+section.graph-display.graph .chart-option.field.category_11 h1,
+section.graph-display.graph .chart-option.field.category_11 h2,
+section.graph-display.graph .chart-option.field.category_11 h3,
+section.graph-display.graph .chart-option.field.category_11 .shortname,
+section.graph-display.graph .chart-option.field.category_11 .name,
+section.graph-display.graph .chart-option.field.category_11 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_11 {
+  color: #eee;
+  background-color: #c9313d;
+}
+section.graph-display.graph .chart-option.field.category_12 {
+  color: #eee;
+  background-color: #ad3238;
+}
+section.graph-display.graph .chart-option.field.category_12 label,
+section.graph-display.graph .chart-option.field.category_12 h1,
+section.graph-display.graph .chart-option.field.category_12 h2,
+section.graph-display.graph .chart-option.field.category_12 h3,
+section.graph-display.graph .chart-option.field.category_12 .shortname,
+section.graph-display.graph .chart-option.field.category_12 .name,
+section.graph-display.graph .chart-option.field.category_12 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_12 {
+  color: #eee;
+  background-color: #ad3238;
+}
+section.graph-display.graph .chart-option.field.category_13 {
+  color: #333;
+  background-color: #ff87ff;
+}
+section.graph-display.graph .chart-option.field.category_13 label,
+section.graph-display.graph .chart-option.field.category_13 h1,
+section.graph-display.graph .chart-option.field.category_13 h2,
+section.graph-display.graph .chart-option.field.category_13 h3,
+section.graph-display.graph .chart-option.field.category_13 .shortname,
+section.graph-display.graph .chart-option.field.category_13 .name,
+section.graph-display.graph .chart-option.field.category_13 .close {
+  color: #333;
+}
+section.graph-display.graph .chart-option.field .tag.category_13 {
+  color: #333;
+  background-color: #ff87ff;
+}
+section.graph-display.graph .chart-option.field.category_14 {
+  color: #eee;
+  background-color: #ff0097;
+}
+section.graph-display.graph .chart-option.field.category_14 label,
+section.graph-display.graph .chart-option.field.category_14 h1,
+section.graph-display.graph .chart-option.field.category_14 h2,
+section.graph-display.graph .chart-option.field.category_14 h3,
+section.graph-display.graph .chart-option.field.category_14 .shortname,
+section.graph-display.graph .chart-option.field.category_14 .name,
+section.graph-display.graph .chart-option.field.category_14 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_14 {
+  color: #eee;
+  background-color: #ff0097;
+}
+section.graph-display.graph .chart-option.field.category_15 {
+  color: #eee;
+  background-color: #553dc9;
+}
+section.graph-display.graph .chart-option.field.category_15 label,
+section.graph-display.graph .chart-option.field.category_15 h1,
+section.graph-display.graph .chart-option.field.category_15 h2,
+section.graph-display.graph .chart-option.field.category_15 h3,
+section.graph-display.graph .chart-option.field.category_15 .shortname,
+section.graph-display.graph .chart-option.field.category_15 .name,
+section.graph-display.graph .chart-option.field.category_15 .close {
+  color: #eee;
+}
+section.graph-display.graph .chart-option.field .tag.category_15 {
+  color: #eee;
+  background-color: #553dc9;
+}
+section.graph-display.graph .chart-option.field.category_16 {
+  color: #333;
+  background-color: #9da5ff;
+}
+section.graph-display.graph .chart-option.field.category_16 label,
+section.graph-display.graph .chart-option.field.category_16 h1,
+section.graph-display.graph .chart-option.field.category_16 h2,
+section.graph-display.graph .chart-option.field.category_16 h3,
+section.graph-display.graph .chart-option.field.category_16 .shortname,
+section.graph-display.graph .chart-option.field.category_16 .name,
+section.graph-display.graph .chart-option.field.category_16 .close {
+  color: #333;
+}
+section.graph-display.graph .chart-option.field .tag.category_16 {
+  color: #333;
+  background-color: #9da5ff;
+}
+section.graph-display.graph .chart-option.field.category_17 {
+  color: #333;
+  background-color: #9323ff;
+}
+section.graph-display.graph .chart-option.field.category_17 label,
+section.graph-display.graph .chart-option.field.category_17 h1,
+section.graph-display.graph .chart-option.field.category_17 h2,
+section.graph-display.graph .chart-option.field.category_17 h3,
+section.graph-display.graph .chart-option.field.category_17 .shortname,
+section.graph-display.graph .chart-option.field.category_17 .name,
+section.graph-display.graph .chart-option.field.category_17 .close {
+  color: #333;
+}
+section.graph-display.graph .chart-option.field .tag.category_17 {
+  color: #333;
+  background-color: #9323ff;
+}
diff --git a/www/css/graph.css b/www/css/graph.css
new file mode 100644 (file)
index 0000000..5914083
--- /dev/null
@@ -0,0 +1,691 @@
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
+section.graph {
+  position: relative;
+  margin: 0 auto;
+  min-width: 640px;
+  max-width: 960px;
+/* * * *  Chart & Viewport  * * * {{{ */
+/* }}} */
+/* * * *  Graph Details & Info Pane  * * * {{{ */
+/* }}} */
+/* * * *  Subnav & Tabs  * * * {{{ */
+/* }}} */
+/* * * *  Chart Options  * * * {{{ */
+/* }}} End Chart Options */
+}
+section.graph * {
+  position: relative;
+}
+section.graph .graph-details > * {
+  margin-left: auto;
+  margin-right: auto;
+}
+section.graph .graph-viewport-row {
+  margin-bottom: 3em;
+}
+section.graph .viewport {
+  position: relative;
+  min-width: 200px;
+  min-height: 320px;
+  overflow: hidden;
+}
+section.graph .callout {
+  float: left;
+  z-index: 100;
+  width: 15.385em;
+  padding: 0 6em 0 0;
+  background-color: #fff;
+  text-align: right;
+}
+section.graph .callout .latest-metric {
+  font-size: 18px;
+  line-height: 36px;
+}
+section.graph .callout .metric-change {
+  font: 11px/1.5 "helvetica neue", helvetica, arial, sans-serif;
+}
+section.graph .callout .metric-change span {
+  display: inline-block;
+}
+section.graph .callout .metric-change span.dates {
+  width: 140px;
+}
+section.graph .callout .metric-change span.value {
+  width: 60px;
+}
+section.graph .graph-legend {
+  position: absolute;
+  top: 1em;
+  right: 1em;
+  z-index: 100;
+  width: 200px;
+  padding: 1em;
+  background-color: rgba(255,255,255,0.75);
+  font: 12px/1.5 "helvetica neue", helvetica, arial, sans-serif;
+  border: 1px solid #eee;
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+section.graph .graph-legend b {
+  display: inline-block;
+  width: 140px;
+}
+section.graph .graph-legend .whole {
+  display: inline-block;
+  width: 30px;
+  text-align: right;
+}
+section.graph .graph-legend .fraction {
+  text-align: left;
+}
+section.graph .graph-legend .suffix {
+  text-align: left;
+}
+section.graph .viewport:hover + .graph-legend {
+  border: 1px solid #eee;
+}
+section.graph .graph-name-row {
+  margin: 0 0 1em;
+  min-height: 38px;
+}
+section.graph .graph-name-row input.graph-name {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  width: auto;
+  font-size: 18px;
+  line-height: 25px;
+  height: 25px;
+  padding: 6px;
+  border-color: #eee;
+}
+section.graph .graph-info-pane .row-fluid .half.control-group {
+  width: 50%;
+  float: left;
+  margin-left: 0;
+  margin-right: 0;
+}
+section.graph .graph-info-pane .row-fluid label {
+  width: 100px;
+}
+section.graph .graph-info-pane .row-fluid .controls {
+  margin-left: 110px;
+}
+section.graph .graph-info-pane .help-block {
+  font-size: 11px;
+  line-height: 1.3;
+}
+section.graph .graph-settings-row {
+  max-width: 900px;
+}
+section.graph .graph-settings.tabbable .graph-settings-nav > .nav {
+  z-index: 3;
+  margin-bottom: 0;
+  border: 0;
+}
+section.graph .graph-settings.tabbable .graph-settings-nav > .nav li h3 {
+  line-height: 14px;
+  margin: 2px;
+  padding: 8px 12px;
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+section.graph .graph-settings.tabbable .graph-settings-nav > .nav li {
+  margin-right: 4px;
+}
+section.graph .graph-settings.tabbable .graph-settings-nav .nav-shadow-shim,
+section.graph .graph-settings.tabbable .graph-settings-nav .nav-shadow {
+  position: absolute;
+  width: 100%;
+}
+section.graph .graph-settings.tabbable .graph-settings-nav .nav-shadow-shim {
+  left: -7px;
+  bottom: -1px;
+  z-index: 2;
+  height: 30px;
+  padding: 0 7px;
+  background-color: #fff;
+  border-bottom: 1px solid #ccc;
+}
+section.graph .graph-settings.tabbable .graph-settings-nav .nav-shadow {
+  left: 0;
+  bottom: 0;
+  z-index: 1;
+  height: 10px;
+  -webkit-box-shadow: 0 0 6px 2px rgba(0,0,0,0.2);
+  -moz-box-shadow: 0 0 6px 2px rgba(0,0,0,0.2);
+  box-shadow: 0 0 6px 2px rgba(0,0,0,0.2);
+}
+section.graph .graph-settings.tabbable .graph-tab-content > .tab-pane {
+  padding: 0.5em;
+  margin-top: 1em;
+  height: 100%;
+}
+section.graph .graph-settings.tabbable .graph-tab-content > .tab-pane.graph-data-pane {
+  margin-top: 0;
+  padding: 0;
+  border-bottom: 1px solid #ddd;
+}
+section.graph .graph-controls {
+  z-index: 100;
+  margin-top: 2px;
+}
+section.graph .graph-controls > .btn,
+section.graph .graph-controls > .btn-group {
+  margin: 0 0.5em;
+}
+section.graph .graph-controls .btn-group {
+  display: inline-block;
+}
+section.graph .graph-controls input[type="button"] {
+  min-width: 5em;
+  text-align: center;
+}
+section.graph .graph-permalink input {
+  cursor: auto;
+}
+section.graph .graph-spinner {
+  display: none;
+  position: absolute;
+  bottom: 3px;
+  left: 0;
+  margin-left: -2em;
+  width: 2.5em;
+  height: 2.5em;
+}
+section.graph .chart-options,
+section.graph .chart-options .fields {
+  width: 100%;
+  min-height: 250px;
+}
+section.graph .chart-options .chart-options-controls {
+  font-size: 11px;
+}
+section.graph .chart-options .chart-options-controls .control-group,
+section.graph .chart-options .chart-options-controls .btn-group {
+  display: inline-block;
+  font-size: 11px;
+}
+section.graph .chart-options .chart-options-controls .btn {
+  font-size: 11px;
+  padding: 3px 9px 3px;
+}
+section.graph .chart-options .chart-options-controls > .btn,
+section.graph .chart-options .chart-options-controls :not(.btn-group) .btn {
+  margin-right: 0.75em;
+}
+section.graph .chart-option.field {
+  float: left;
+  z-index: 3;
+  padding: 0.5em;
+  margin: 0.4em;
+  min-width: 200px;
+  max-width: 250px;
+  min-height: 1.5em;
+  line-height: 1.5;
+  overflow: hidden;
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+  background-color: #ccc;
+  font-size: 90%;
+/* Category/Tag Colors {{{ */
+/* }}} */
+}
+section.graph .chart-option.field h3 {
+  font-size: 14px;
+  line-height: 1.3;
+  cursor: pointer;
+}
+section.graph .chart-option.field .close {
+  position: absolute;
+  top: 0;
+  right: 0.1em;
+  width: 1em;
+  height: 1em;
+  line-height: 1.2em;
+  text-align: center;
+  text-decoration: none;
+  z-index: 10;
+  cursor: pointer;
+  opacity: 0.3;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=30);
+}
+section.graph .chart-option.field .close:hover {
+  opacity: 0.6;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60);
+}
+section.graph .chart-option.field input.value:not([type="checkbox"]) {
+  width: 240px;
+  font-family: menlo, monospace;
+}
+section.graph .chart-option.field textarea {
+  resize: none;
+}
+section.graph .chart-option.field .shortname {
+  font-weight: bold;
+  color: #fff;
+  min-height: 1.5em;
+}
+section.graph .chart-option.field .name {
+  display: none;
+  font-weight: bold;
+}
+section.graph .chart-option.field .type::before {
+  content: "Type: ";
+  font-weight: bold;
+}
+section.graph .chart-option.field .default::before {
+  content: "Default: ";
+  font-weight: bold;
+}
+section.graph .chart-option.field .desc {
+  position: relative;
+}
+section.graph .chart-option.field .tags,
+section.graph .chart-option.field .examples {
+  cursor: pointer;
+}
+section.graph .chart-option.field .tags {
+  font-size: 85%;
+}
+section.graph .chart-option.field .tags::before {
+  content: "Tags: ";
+  font-weight: bold;
+}
+section.graph .chart-option.field .tags .tag {
+  margin: 0.2em;
+  line-height: 1.5;
+  padding: 0.2em;
+  white-space: nowrap;
+  color: #fff;
+  background-color: rgba(255,255,255,0.15);
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+section.graph .chart-option.field .examples {
+  display: none;
+}
+section.graph .chart-option.field .examples::before {
+  content: "Examples";
+  font-weight: bold;
+}
+section.graph .chart-option.field .examples .example {
+  position: relative;
+}
+section.graph .chart-option.field.collapsed {
+  z-index: 2;
+  width: auto;
+  min-width: 50px;
+  min-height: 2em;
+  max-width: none;
+  line-height: 2;
+  cursor: pointer;
+  text-align: center;
+}
+section.graph .chart-option.field.collapsed * {
+  display: none;
+}
+section.graph .chart-option.field.collapsed .shortname {
+  display: inline-block;
+  min-width: 50px;
+  min-height: auto;
+}
+section.graph .chart-option.field.category_0 {
+  color: #eee;
+  background-color: #182b53;
+}
+section.graph .chart-option.field.category_0 label,
+section.graph .chart-option.field.category_0 h1,
+section.graph .chart-option.field.category_0 h2,
+section.graph .chart-option.field.category_0 h3,
+section.graph .chart-option.field.category_0 .shortname,
+section.graph .chart-option.field.category_0 .name,
+section.graph .chart-option.field.category_0 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_0 {
+  color: #eee;
+  background-color: #182b53;
+}
+section.graph .chart-option.field.category_1 {
+  color: #eee;
+  background-color: #0080ff;
+}
+section.graph .chart-option.field.category_1 label,
+section.graph .chart-option.field.category_1 h1,
+section.graph .chart-option.field.category_1 h2,
+section.graph .chart-option.field.category_1 h3,
+section.graph .chart-option.field.category_1 .shortname,
+section.graph .chart-option.field.category_1 .name,
+section.graph .chart-option.field.category_1 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_1 {
+  color: #eee;
+  background-color: #0080ff;
+}
+section.graph .chart-option.field.category_2 {
+  color: #333;
+  background-color: #a6d9ff;
+}
+section.graph .chart-option.field.category_2 label,
+section.graph .chart-option.field.category_2 h1,
+section.graph .chart-option.field.category_2 h2,
+section.graph .chart-option.field.category_2 h3,
+section.graph .chart-option.field.category_2 .shortname,
+section.graph .chart-option.field.category_2 .name,
+section.graph .chart-option.field.category_2 .close {
+  color: #333;
+}
+section.graph .chart-option.field .tag.category_2 {
+  color: #333;
+  background-color: #a6d9ff;
+}
+section.graph .chart-option.field.category_3 {
+  color: #333;
+  background-color: #51e3c5;
+}
+section.graph .chart-option.field.category_3 label,
+section.graph .chart-option.field.category_3 h1,
+section.graph .chart-option.field.category_3 h2,
+section.graph .chart-option.field.category_3 h3,
+section.graph .chart-option.field.category_3 .shortname,
+section.graph .chart-option.field.category_3 .name,
+section.graph .chart-option.field.category_3 .close {
+  color: #333;
+}
+section.graph .chart-option.field .tag.category_3 {
+  color: #333;
+  background-color: #51e3c5;
+}
+section.graph .chart-option.field.category_4 {
+  color: #eee;
+  background-color: #254a59;
+}
+section.graph .chart-option.field.category_4 label,
+section.graph .chart-option.field.category_4 h1,
+section.graph .chart-option.field.category_4 h2,
+section.graph .chart-option.field.category_4 h3,
+section.graph .chart-option.field.category_4 .shortname,
+section.graph .chart-option.field.category_4 .name,
+section.graph .chart-option.field.category_4 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_4 {
+  color: #eee;
+  background-color: #254a59;
+}
+section.graph .chart-option.field.category_5 {
+  color: #eee;
+  background-color: #32938a;
+}
+section.graph .chart-option.field.category_5 label,
+section.graph .chart-option.field.category_5 h1,
+section.graph .chart-option.field.category_5 h2,
+section.graph .chart-option.field.category_5 h3,
+section.graph .chart-option.field.category_5 .shortname,
+section.graph .chart-option.field.category_5 .name,
+section.graph .chart-option.field.category_5 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_5 {
+  color: #eee;
+  background-color: #32938a;
+}
+section.graph .chart-option.field.category_6 {
+  color: #eee;
+  background-color: #83bb32;
+}
+section.graph .chart-option.field.category_6 label,
+section.graph .chart-option.field.category_6 h1,
+section.graph .chart-option.field.category_6 h2,
+section.graph .chart-option.field.category_6 h3,
+section.graph .chart-option.field.category_6 .shortname,
+section.graph .chart-option.field.category_6 .name,
+section.graph .chart-option.field.category_6 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_6 {
+  color: #eee;
+  background-color: #83bb32;
+}
+section.graph .chart-option.field.category_7 {
+  color: #333;
+  background-color: #b1e43b;
+}
+section.graph .chart-option.field.category_7 label,
+section.graph .chart-option.field.category_7 h1,
+section.graph .chart-option.field.category_7 h2,
+section.graph .chart-option.field.category_7 h3,
+section.graph .chart-option.field.category_7 .shortname,
+section.graph .chart-option.field.category_7 .name,
+section.graph .chart-option.field.category_7 .close {
+  color: #333;
+}
+section.graph .chart-option.field .tag.category_7 {
+  color: #333;
+  background-color: #b1e43b;
+}
+section.graph .chart-option.field.category_8 {
+  color: #333;
+  background-color: #f1d950;
+}
+section.graph .chart-option.field.category_8 label,
+section.graph .chart-option.field.category_8 h1,
+section.graph .chart-option.field.category_8 h2,
+section.graph .chart-option.field.category_8 h3,
+section.graph .chart-option.field.category_8 .shortname,
+section.graph .chart-option.field.category_8 .name,
+section.graph .chart-option.field.category_8 .close {
+  color: #333;
+}
+section.graph .chart-option.field .tag.category_8 {
+  color: #333;
+  background-color: #f1d950;
+}
+section.graph .chart-option.field.category_9 {
+  color: #333;
+  background-color: #ef8158;
+}
+section.graph .chart-option.field.category_9 label,
+section.graph .chart-option.field.category_9 h1,
+section.graph .chart-option.field.category_9 h2,
+section.graph .chart-option.field.category_9 h3,
+section.graph .chart-option.field.category_9 .shortname,
+section.graph .chart-option.field.category_9 .name,
+section.graph .chart-option.field.category_9 .close {
+  color: #333;
+}
+section.graph .chart-option.field .tag.category_9 {
+  color: #333;
+  background-color: #ef8158;
+}
+section.graph .chart-option.field.category_10 {
+  color: #eee;
+  background-color: #dc3522;
+}
+section.graph .chart-option.field.category_10 label,
+section.graph .chart-option.field.category_10 h1,
+section.graph .chart-option.field.category_10 h2,
+section.graph .chart-option.field.category_10 h3,
+section.graph .chart-option.field.category_10 .shortname,
+section.graph .chart-option.field.category_10 .name,
+section.graph .chart-option.field.category_10 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_10 {
+  color: #eee;
+  background-color: #dc3522;
+}
+section.graph .chart-option.field.category_11 {
+  color: #eee;
+  background-color: #c9313d;
+}
+section.graph .chart-option.field.category_11 label,
+section.graph .chart-option.field.category_11 h1,
+section.graph .chart-option.field.category_11 h2,
+section.graph .chart-option.field.category_11 h3,
+section.graph .chart-option.field.category_11 .shortname,
+section.graph .chart-option.field.category_11 .name,
+section.graph .chart-option.field.category_11 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_11 {
+  color: #eee;
+  background-color: #c9313d;
+}
+section.graph .chart-option.field.category_12 {
+  color: #eee;
+  background-color: #ad3238;
+}
+section.graph .chart-option.field.category_12 label,
+section.graph .chart-option.field.category_12 h1,
+section.graph .chart-option.field.category_12 h2,
+section.graph .chart-option.field.category_12 h3,
+section.graph .chart-option.field.category_12 .shortname,
+section.graph .chart-option.field.category_12 .name,
+section.graph .chart-option.field.category_12 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_12 {
+  color: #eee;
+  background-color: #ad3238;
+}
+section.graph .chart-option.field.category_13 {
+  color: #333;
+  background-color: #ff87ff;
+}
+section.graph .chart-option.field.category_13 label,
+section.graph .chart-option.field.category_13 h1,
+section.graph .chart-option.field.category_13 h2,
+section.graph .chart-option.field.category_13 h3,
+section.graph .chart-option.field.category_13 .shortname,
+section.graph .chart-option.field.category_13 .name,
+section.graph .chart-option.field.category_13 .close {
+  color: #333;
+}
+section.graph .chart-option.field .tag.category_13 {
+  color: #333;
+  background-color: #ff87ff;
+}
+section.graph .chart-option.field.category_14 {
+  color: #eee;
+  background-color: #ff0097;
+}
+section.graph .chart-option.field.category_14 label,
+section.graph .chart-option.field.category_14 h1,
+section.graph .chart-option.field.category_14 h2,
+section.graph .chart-option.field.category_14 h3,
+section.graph .chart-option.field.category_14 .shortname,
+section.graph .chart-option.field.category_14 .name,
+section.graph .chart-option.field.category_14 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_14 {
+  color: #eee;
+  background-color: #ff0097;
+}
+section.graph .chart-option.field.category_15 {
+  color: #eee;
+  background-color: #553dc9;
+}
+section.graph .chart-option.field.category_15 label,
+section.graph .chart-option.field.category_15 h1,
+section.graph .chart-option.field.category_15 h2,
+section.graph .chart-option.field.category_15 h3,
+section.graph .chart-option.field.category_15 .shortname,
+section.graph .chart-option.field.category_15 .name,
+section.graph .chart-option.field.category_15 .close {
+  color: #eee;
+}
+section.graph .chart-option.field .tag.category_15 {
+  color: #eee;
+  background-color: #553dc9;
+}
+section.graph .chart-option.field.category_16 {
+  color: #333;
+  background-color: #9da5ff;
+}
+section.graph .chart-option.field.category_16 label,
+section.graph .chart-option.field.category_16 h1,
+section.graph .chart-option.field.category_16 h2,
+section.graph .chart-option.field.category_16 h3,
+section.graph .chart-option.field.category_16 .shortname,
+section.graph .chart-option.field.category_16 .name,
+section.graph .chart-option.field.category_16 .close {
+  color: #333;
+}
+section.graph .chart-option.field .tag.category_16 {
+  color: #333;
+  background-color: #9da5ff;
+}
+section.graph .chart-option.field.category_17 {
+  color: #333;
+  background-color: #9323ff;
+}
+section.graph .chart-option.field.category_17 label,
+section.graph .chart-option.field.category_17 h1,
+section.graph .chart-option.field.category_17 h2,
+section.graph .chart-option.field.category_17 h3,
+section.graph .chart-option.field.category_17 .shortname,
+section.graph .chart-option.field.category_17 .name,
+section.graph .chart-option.field.category_17 .close {
+  color: #333;
+}
+section.graph .chart-option.field .tag.category_17 {
+  color: #333;
+  background-color: #9323ff;
+}
diff --git a/www/css/hicons.css b/www/css/hicons.css
new file mode 100644 (file)
index 0000000..79f89e5
--- /dev/null
@@ -0,0 +1,106 @@
+[class^="hicon-"],
+[class*=" hicon-"] {
+  display: inline-block;
+  vertical-align: text-top;
+  background-repeat: no-repeat;
+  background-image: url("/img/hicons/hicons-sprite-black.png");
+  width: 32px;
+  height: 32px;
+  line-height: 32px;
+  background-position: 32px 32px;
+  *margin-right: 0.3em;
+}
+[class^="hicon-"][class~="icon-white"],
+[class*=" hicon-"][class~="icon-white"] {
+  background-image: url("/img/hicons/hicons-sprite-white.png");
+}
+[class^="hicon-"][class~="icon-sm"],
+[class*=" hicon-"][class~="icon-sm"] {
+  width: 16px;
+  height: 16px;
+  line-height: 16px;
+  background-position: 16px 16px;
+}
+[class^="hicon-"][class~="icon-med"],
+[class*=" hicon-"][class~="icon-med"] {
+  width: 24px;
+  height: 24px;
+  line-height: 24px;
+  background-position: 24px 24px;
+}
+[class^="hicon-"]:last-child,
+[class*=" hicon-"]:last-child {
+  *margin-left: 0;
+}
+.hicon-chart-bar {
+  background-position: 0px -24px;
+}
+.hicon-chart-curve {
+  background-position: 0px -56px;
+}
+.hicon-chart-line {
+  background-position: -32px -56px;
+}
+.hicon-chart-pie {
+  background-position: 0px -112px;
+}
+.hicon-server-db {
+  background-position: 0px -144px;
+}
+.hicon-settings {
+  background-position: -32px -144px;
+}
+.hicon-split-h {
+  background-position: 0px -200px;
+}
+.hicon-split-v {
+  background-position: -32px -200px;
+}
+[class~="icon-sm"].hicon-chart-bar {
+  background-position: -24px 0px;
+}
+[class~="icon-sm"].hicon-chart-curve {
+  background-position: -32px -24px;
+}
+[class~="icon-sm"].hicon-chart-line {
+  background-position: -32px -40px;
+}
+[class~="icon-sm"].hicon-chart-pie {
+  background-position: -24px -88px;
+}
+[class~="icon-sm"].hicon-server-db {
+  background-position: -32px -112px;
+}
+[class~="icon-sm"].hicon-settings {
+  background-position: -32px -128px;
+}
+[class~="icon-sm"].hicon-split-h {
+  background-position: -24px -176px;
+}
+[class~="icon-sm"].hicon-split-v {
+  background-position: 0px -232px;
+}
+[class~="icon-med"].hicon-chart-bar {
+  background-position: 0px 0px;
+}
+[class~="icon-med"].hicon-chart-curve {
+  background-position: -40px 0px;
+}
+[class~="icon-med"].hicon-chart-line {
+  background-position: -48px -24px;
+}
+[class~="icon-med"].hicon-chart-pie {
+  background-position: 0px -88px;
+}
+[class~="icon-med"].hicon-server-db {
+  background-position: -40px -88px;
+}
+[class~="icon-med"].hicon-settings {
+  background-position: -48px -112px;
+}
+[class~="icon-med"].hicon-split-h {
+  background-position: 0px -176px;
+}
+[class~="icon-med"].hicon-split-v {
+  background-position: -40px -176px;
+}
diff --git a/www/css/layout.css b/www/css/layout.css
new file mode 100644 (file)
index 0000000..62aa8ba
--- /dev/null
@@ -0,0 +1,422 @@
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
+.clearer {
+  zoom: 1;
+}
+.clearer:before,
+.clearer:after {
+  content: "";
+  display: table;
+}
+.clearer:after {
+  clear: both;
+}
+.icon-wmf {
+  display: inline-block;
+  width: 17px;
+  height: 17px;
+  line-height: 16px;
+  background: transparent no-repeat 0 0;
+  background-image: url("/img/wmf_logo/wmf_logo-white-16x16.png") !important;
+}
+.image {
+  display: block;
+  overflow: hidden;
+  text-indent: -9999px;
+  background: transparent no-repeat 0 0;
+  border: 0 !important;
+}
+.image.wmf-logo {
+  width: 45px;
+  height: 45px;
+  line-height: 45px;
+  background-image: url("/img/wmf_logo/wmf_logo-black-45x45-a100.png");
+  opacity: 0.1;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=10);
+}
+.image.wmf-logo:hover {
+  opacity: 0.25;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=25);
+}
+.image.public-domain {
+  width: 31px;
+  height: 32px;
+  line-height: 32px;
+  background-image: url("/img/public_domain/public_domain-31x32-a100.png");
+  opacity: 0.12;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=12);
+}
+.image.public-domain:hover {
+  opacity: 0.25;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=25);
+}
+html,
+body {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+}
+html {
+  min-height: 100%;
+}
+body {
+  min-height: 100%;
+}
+header,
+footer,
+#content {
+  position: relative;
+  padding: 0.5em;
+}
+#content {
+  z-index: 10;
+  padding: 1em;
+  min-height: 500px;
+  background-color: #fff;
+  -webkit-box-shadow: 0 0 8px 5px rgba(0,0,0,0.45);
+  -moz-box-shadow: 0 0 8px 5px rgba(0,0,0,0.45);
+  box-shadow: 0 0 8px 5px rgba(0,0,0,0.45);
+}
+#content > .inner {
+  z-index: 12;
+  position: relative;
+  margin: 0 auto;
+}
+#content > .inner.fluid-inner {
+  width: 80%;
+  min-width: 960px;
+  max-width: none;
+}
+#content > .inner .centered {
+  margin: 0 auto;
+  max-width: 960px;
+}
+#content .spacer {
+  font-size: 250%;
+  margin-top: 1em;
+  padding-top: 1em;
+}
+#content .notice {
+  position: absolute;
+  top: 1.25em;
+  left: 50%;
+  margin-left: -25%;
+  width: 50%;
+  padding: 0.25em;
+  -webkit-border-radius: 0.5em;
+  -moz-border-radius: 0.5em;
+  border-radius: 0.5em;
+  text-align: center;
+}
+#content .notice.info {
+  color: #fff;
+  background: #4596ff;
+}
+#content .notice.error {
+  color: #fff;
+  background: #af2a31;
+}
+nav#main-nav {
+  position: absolute;
+  top: 0;
+  right: 16px;
+  height: 100%;
+}
+nav#main-nav ul,
+nav#main-nav li {
+  position: relative;
+  top: 0;
+  left: 0;
+}
+nav#main-nav ul {
+  height: 100%;
+  top: 50%;
+}
+nav#main-nav li {
+  display: inline-block;
+  top: -0.75em;
+  margin: 0 0.5em;
+}
+.nav a,
+.nav a:link,
+.nav a:visited,
+.nav a:active {
+  text-decoration: none;
+}
+.nav.nav-pills {
+  background-color: #fff;
+  border-bottom: 1px solid #ccc;
+  padding-bottom: 0.25em;
+}
+header {
+  z-index: 1000;
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  width: 100%;
+  height: auto;
+  background-color: #eee;
+  -webkit-box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.3);
+  -moz-box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.3);
+  box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.3);
+}
+footer {
+  z-index: 1;
+  position: relative;
+  width: 800px;
+  margin: 0 auto;
+  background-color: #3b3b3b;
+}
+footer .icon-white {
+  opacity: 0.5;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50);
+  margin-right: 5px;
+}
+footer h3,
+footer h4,
+footer h5 {
+  margin-bottom: 0.75em;
+}
+footer > .row,
+footer > .row-fluid {
+  position: relative;
+  margin: 45px auto;
+}
+footer > .row .inner,
+footer > .row-fluid .inner {
+  position: relative;
+  max-width: 410px;
+  margin: 0 auto;
+}
+footer .info-row {
+  margin: 45px auto;
+}
+footer .info-row p {
+  margin: 0 1em 0.9em;
+}
+footer .info-row ul.site-level {
+  list-style: none;
+  margin-left: 10px;
+}
+footer .info-row ul ul {
+  margin-left: 35px;
+}
+footer .copyright-row {
+  margin: 65px auto 0;
+  font-size: 11px;
+}
+footer .copyright-row .public-domain {
+  display: block;
+  position: absolute;
+  top: 50%;
+  left: 10px;
+  margin-top: -15px;
+}
+footer .copyright-row .copyright {
+  padding-left: 50px;
+}
+footer .tos-row {
+  margin: 0 auto;
+  font-size: 11px;
+  line-height: 24px;
+  text-align: center;
+  vertical-align: middle;
+}
+footer .tos-row * {
+  display: inline-block;
+  line-height: 22px;
+  vertical-align: middle;
+  margin: 0 5px;
+}
+footer .tos-row .separator {
+  color: #000;
+  font-size: 22px;
+  opacity: 0.25;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=25);
+}
+footer .tos-row a:not(.btn) {
+  border: 0;
+}
+footer .wmf-logo {
+  margin: 65px auto;
+}
+.rilke {
+  position: absolute;
+  bottom: 1em;
+  right: 1em;
+  z-index: 30;
+  width: 185px;
+  opacity: 0.1;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=10);
+  -webkit-transition-duration: 0.8s;
+  -moz-transition-duration: 0.8s;
+  transition-duration: 0.8s;
+  -webkit-transition-property: opacity;
+  -moz-transition-property: opacity;
+  transition-property: opacity;
+}
+.rilke:hover {
+  opacity: 1;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
+}
+.rilke,
+.rilke * {
+  font-size: 10px;
+  line-height: 1.3;
+  text-indent: -1em;
+  border: 0;
+  color: #666;
+}
+.rilke small {
+  margin-top: 5px;
+  text-align: right;
+  color: #888;
+}
+.rilke blockquote {
+  margin: 0;
+  padding: 0;
+}
diff --git a/www/css/mixins.css b/www/css/mixins.css
new file mode 100644 (file)
index 0000000..cae2aff
--- /dev/null
@@ -0,0 +1,48 @@
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
diff --git a/www/css/text.css b/www/css/text.css
new file mode 100644 (file)
index 0000000..cae2aff
--- /dev/null
@@ -0,0 +1,48 @@
+html,
+body {
+  font-family: helvetica, arial, sans-serif;
+  line-height: 1.5;
+  color: #333;
+  background-color: #3b3b3b;
+}
+h1,
+h2,
+h3 {
+  font-family: "gotham rounded", "helvetica neue", helvetica, arial, sans-serif;
+  color: #333;
+  font-weight: 300;
+}
+h1 a:not(.btn),
+h2 a:not(.btn),
+h3 a:not(.btn) {
+  color: #333;
+  text-decoration: none;
+}
+h1 a:not(.btn):hover,
+h2 a:not(.btn):hover,
+h3 a:not(.btn):hover {
+  color: #333;
+}
+footer {
+  color: #666;
+}
+footer a:not(.btn) {
+  color: #888;
+  border-bottom: 1px solid rgba(255,255,255,0.15);
+}
+footer a:not(.btn):hover {
+  border: none;
+}
+footer h3,
+footer h4 {
+  font-size: 16px;
+  color: #888 !important;
+}
+footer h3 a:link,
+footer h4 a:link,
+footer h3 a:visited,
+footer h4 a:visited,
+footer h3 a:active,
+footer h4 a:active {
+  color: #888 !important;
+}
diff --git a/www/schema/d3/d3-bar.json b/www/schema/d3/d3-bar.json
new file mode 100644 (file)
index 0000000..dd3ac34
--- /dev/null
@@ -0,0 +1 @@
+[{"name":"zoom.start","tags":["interactivity","zoom"],"type":"Float","default":1,"desc":"Initial zoom-level, where 1.0 (100% zoom) shows the full map in the frame (the default)."}]
\ No newline at end of file
diff --git a/www/schema/d3/d3-chart.json b/www/schema/d3/d3-chart.json
new file mode 100644 (file)
index 0000000..dd3ac34
--- /dev/null
@@ -0,0 +1 @@
+[{"name":"zoom.start","tags":["interactivity","zoom"],"type":"Float","default":1,"desc":"Initial zoom-level, where 1.0 (100% zoom) shows the full map in the frame (the default)."}]
\ No newline at end of file
diff --git a/www/schema/d3/d3-geo-world.json b/www/schema/d3/d3-geo-world.json
new file mode 100644 (file)
index 0000000..421b4fa
--- /dev/null
@@ -0,0 +1 @@
+[{"name":"zoom.start","tags":["interactivity","zoom"],"type":"Float","default":1,"desc":"Initial zoom-level, where 1.0 (100% zoom) shows the full map in the frame (the default)."},{"name":"zoom.min","tags":["interactivity","zoom"],"type":"Float","default":1,"desc":"Limit to the amount the chart will zoom out, expressed as a multiplier of the frame. By default, this is limited to show the whole map in the frame."},{"name":"zoom.max","tags":["interactivity","zoom"],"type":"Float","default":8,"desc":"Limit to the amount the chart will zoom in, expressed as a multiplier of the frame (8x by default)."},{"name":"colors.palette","tags":["color","axes","standard"],"type":"Array","default":["black","red"],"desc":"Array of colors to which values are mapped (based on their position in `colors.scaleDomain`)."},{"name":"colors.scale","tags":["color","axes","standard"],"type":"enum","values":["linear","log"],"default":"log","desc":"Scale color differences in the map using a scale-transform (log-scale bydefault). Options include:- Linear scaling- Logarithmic scaling"},{"name":"colors.scaleDomain","tags":["color","axes"],"type":"Array","default":null,"desc":"Domain for scaling color differences. Uses the extent of the dataset by default (and when `null`), meaning the smallest value will map to the first color of the palette, and the largest value to the last color."},{"name":"colors.missing","tags":["standard","color","data"],"type":"String","default":"rgba(0,0,0,0)","desc":"Features without values are replaced with this color (transparent by default)."},{"name":"map.projection","tags":["geo","map"],"type":"enum","values":["mercator","albers","albersUsa","azimuthalOrtho","azimuthalStereo"],"default":"mercator","desc":"Projection for map-data (mercator by default). Options include:- Spherical mercator projection- Albers equal-area conic projection- Composite Albers projection for the United States- Orthographic Azimuthal projection- Stereographic Azimuthal projection"},{"name":"map.definition","tags":["geo","map"],"type":"String","default":"/data/geo/maps/world-countries.json","desc":"Path or URL to the `geoJSON` map definition data."}]
\ No newline at end of file
diff --git a/www/schema/d3/d3-line.json b/www/schema/d3/d3-line.json
new file mode 100644 (file)
index 0000000..dd3ac34
--- /dev/null
@@ -0,0 +1 @@
+[{"name":"zoom.start","tags":["interactivity","zoom"],"type":"Float","default":1,"desc":"Initial zoom-level, where 1.0 (100% zoom) shows the full map in the frame (the default)."}]
\ No newline at end of file
diff --git a/www/schema/dygraph.json b/www/schema/dygraph.json
new file mode 100644 (file)
index 0000000..0cdeee0
--- /dev/null
@@ -0,0 +1 @@
+[{"name":"dateWindow","tags":["interactivity","axes"],"type":"Array of two Dates or numbers","default":null,"desc":"Initially zoom in on a section of the graph. Is of the form [earliest, latest], where earliest/latest are milliseconds since epoch. If the data for the x-axis is numeric, the values in dateWindow must also be numbers. By default, the full range of the input is shown.","examples":["dateWindow","drawing","is-zoomed-ignore-programmatic-zoom","link-interaction","synchronize","zoom"]},{"name":"visibility","tags":["lines","data","display","graph"],"type":"Array of booleans","default":null,"desc":"Which series should initially be visible? Once the Dygraph has been constructed, you can access and modify the visibility of each series using the visibility and setVisibility methods.","examples":["color-visibility","no-visibility","visibility"]},{"name":"strokeWidth","tags":["lines","data","display","line","standard"],"type":"Integer","default":1,"desc":"The width of the lines connecting data points. This can be used to increase the contrast or some graphs.","examples":["linear-regression-addseries","drawing","grid_dot","layout-options","linear-regression-fractions","linear-regression","per-series","unboxed-spark","styled-chart-labels"]},{"name":"drawPoints","tags":["lines","points","data","display","standard"],"type":"Boolean","default":false,"desc":"Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart.","examples":["linear-regression-addseries","draw-points","dynamic-update","independent-series","interaction","linear-regression-fractions","linear-regression","per-series"]},{"name":"pointSize","tags":["lines","data","display","points","standard"],"type":"Integer","default":1,"desc":"The size of the dot to draw on each point in pixels (see drawPoints). A dot is always drawn when a point is \"isolated\", i.e. there is a missing point on either side of it. This also controls the size of those dots.","examples":["per-series"]},{"name":"stackedGraph","tags":["lines","data","display","graph","standard"],"type":"Boolean","default":false,"desc":"If set, stack series on top of one another rather than drawing them independently.","examples":["stacked"]},{"name":"fillGraph","tags":["lines","data","display","graph","standard"],"type":"Boolean","default":false,"desc":"Should the area underneath the graph be filled? This option is not compatible with error bars.","examples":["fillGraph","two-axes","steps"]},{"name":"strokePattern","tags":["lines","data","display"],"type":"Array","default":null,"desc":"A custom pattern array where the even index is a draw and odd is a space in pixels. If null then it draws a solid line. The array should have a even length as any odd lengthed array could be expressed as a smaller even length array.","examples":["per-series"]},{"name":"stepPlot","tags":["lines","data","display","graph","standard"],"type":"Boolean","default":false,"desc":"When set, display the graph as a step plot instead of a line plot.","examples":["avoidMinZero","steps","y-axis-formatter"]},{"name":"connectSeparatedPoints","tags":["lines","points","data","display"],"type":"Boolean","default":false,"desc":"Usually, when Dygraphs encounters a missing value in a data series, it interprets this as a gap and draws it as such. If, instead, the missing values represents an x-value for which only a different series has data, then you'll want to connect the dots by setting this to true. To explicitly include a gap with this option set, use a value of NaN.","examples":["connect-separated","independent-series"]},{"name":"rightGap","tags":["display","data","lines","points","graph"],"type":"Integer","default":5,"desc":"Number of pixels to leave blank at the right edge of the Dygraph. This makes it easier to highlight the right-most data point."},{"name":"colors","tags":["color","data","display"],"type":"Array","default":null,"desc":"List of colors for the data series. These can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\", etc. If not specified, equally-spaced points around a color wheel are used.","examples":["century-scale","color-visibility","demo","reverse-y-axis","color-cycle"]},{"name":"colorSaturation","tags":["color","data","display"],"type":"Float (0.0 - 1.0)","default":1,"desc":"If colors is not specified, saturation of the automatically-generated data series colors."},{"name":"colorValue","tags":["color","data","display"],"type":"Float (0.0 - 1.0)","default":0.5,"desc":"If colors is not specified, value of the data series colors, as in hue/saturation/value. (0.0-1.0, default 0.5)"},{"name":"animatedZooms","tags":["interactivity","zoom"],"type":"Boolean","default":false,"desc":"Set this option to animate the transition between zoom windows. Applies to programmatic and interactive zooms. Note that if you also set a drawCallback, it will be called several times on each zoom. If you set a zoomCallback, it will only be called after the animation is complete.","examples":["highlighted-region","link-interaction"]},{"name":"highlightCircleSize","tags":["interactivity","graph"],"type":"Integer","default":3,"desc":"The size in pixels of the dot drawn over highlighted points.","examples":["dygraph-many-points-benchmark","grid_dot","per-series","unboxed-spark"]},{"name":"interactionModel","ignore":true,"tags":["interactivity"],"type":"Object","default":null,"desc":"TODO(konigsberg): document this","examples":["drawing","interaction"]},{"name":"isZoomedIgnoreProgrammaticZoom","tags":["interactivity","zoom"],"type":"Boolean","default":false,"desc":"When this option is passed to updateOptions() along with either the dateWindow or valueRange options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the isZoomed method to determine this.","examples":["is-zoomed-ignore-programmatic-zoom"]},{"name":"labels","tags":["labels","display"],"type":"Array","default":null,"desc":"A name for each data series, including the independent (X) series. For CSV files and DataTable objections, this is determined by context. For raw data, this must be specified. If it is not, default values are supplied and a warning is logged. By default, labels roughly follow [\"X\", \"Y1\", \"Y2\", ...]*","examples":["linear-regression-addseries","connect-separated","drawing","dygraph","dygraph-many-points-benchmark","dynamic-update","highlighted-region","independent-series","isolated-points","label-div","link-interaction","linear-regression","negative","missing-data","native-format","two-axes","perf","small-range-zero","steps","y-axis-formatter","annotation-native","multi-scale","two-axes-vr","value-axis-formatters"]},{"name":"xlabel","tags":["labels","x-axis","display","axes","standard"],"type":"String","default":null,"desc":"Text to display below the chart's x-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-xlabel' classes.","examples":["border","demo","styled-chart-labels","multi-scale"]},{"name":"ylabel","tags":["labels","y-axis","display","axes","standard"],"type":"String","default":null,"desc":"Text to display to the left of the chart's y-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-ylabel' classes. The text will be rotated 90 degrees by default, so CSS rules may behave in unintuitive ways. No additional space is set aside for a y-axis label. If you need more space, increase the width of the y-axis tick labels using the yAxisLabelWidth option. If you need a wider div for the y-axis label, either style it that way with CSS (but remember that it's rotated, so width is controlled by the 'height' property) or set the yLabelWidth option.","examples":["border","demo","two-axes","noise","styled-chart-labels","multi-scale","range-selector","temperature-sf-ny","two-axes-vr"]},{"name":"y2label","tags":["labels","y-axis","display","standard"],"type":"String","default":null,"desc":"Text to display to the right of the chart's secondary y-axis. This label is only displayed if a secondary y-axis is present. See this test for an example of how to do this. The comments for the 'ylabel' option generally apply here as well. This label gets a 'dygraph-y2label' instead of a 'dygraph-ylabel' class.","examples":["two-axes","two-axes-vr"]},{"name":"legend","tags":["legend","labels","display"],"type":"String","default":"onmouseover","desc":"When to display the legend. By default, it only appears when a user mouses over the chart. Set it to \"always\" to always display a legend of some sort.","examples":["demo","noise","per-series","styled-chart-labels","multi-scale","range-selector","temperature-sf-ny"]},{"name":"labelsSeparateLines","tags":["legend","labels","display"],"type":"Boolean","default":false,"desc":"Put &lt;br/&gt; between lines in the label string. Often used in conjunction with labelsDiv.","examples":["century-scale","customLabel","demo","reverse-y-axis"]},{"name":"labelsShowZeroValues","tags":["legend","labels","display"],"type":"Boolean","default":true,"desc":"Show zero value labels in the labelsDiv.","examples":["label-div"]},{"name":"hideOverlayOnMouseOut","tags":["interactivity","legend","labels","display"],"type":"Boolean","default":true,"desc":"Whether to hide the legend when the mouse leaves the chart area.","examples":["gviz-selection"]},{"name":"showLabelsOnHighlight","tags":["interactivity","legend","labels","display"],"type":"Boolean","default":true,"desc":"Whether to show the legend upon mouseover.","examples":["callback"]},{"name":"labelsDiv","ignore":true,"tags":["legend","labels","display"],"type":"DOM element or string","default":null,"desc":"Show data labels in an external div, rather than on the graph.  This value can either be a div element or a div id.","examples":["century-scale","demo","label-div","reverse-y-axis","unboxed-spark"]},{"name":"labelsDivStyles","ignore":true,"tags":["legend","labels","display"],"type":"Object","default":null,"desc":"Additional styles to apply to the currently-highlighted points div. Forexample, { 'font-weight': 'bold' } will make the labels bold.","examples":["border","customLabel","noise","styled-chart-labels","range-selector","temperature-sf-ny"]},{"name":"labelsDivWidth","ignore":true,"tags":["legend","labels","display"],"type":"Integer","default":250,"desc":"Width (in pixels) of the div which shows information on the currently-highlighted points.","examples":["customLabel","noise"]},{"name":"logscale","tags":["axes","y-axis","display","standard"],"type":"Boolean","default":false,"desc":"When set for a y-axis, the graph shows that axis in log scale. Any values less than or equal to zero are not displayed. Not compatible with showZero, and ignores connectSeparatedPoints. Also, showing log scale with valueRanges that are less than zero will result in an unviewable graph.","examples":["logscale","stock"]},{"name":"axis","tags":["axes"],"type":"String or Object","default":null,"desc":"Set to either an object ({}) filled with options for this axis or to the name of an existing data series with its own axis to re-use that axis. See tests for usage.","examples":["two-axes","steps","two-axes-vr","value-axis-formatters"]},{"name":"valueRange","tags":["axes","y-axis","range","interactivity"],"type":"Array of two numbers","default":null,"desc":"Explicitly set the vertical range of the graph to [low, high]. This may be set on a per-axis basis to define each y-axis separately. By default, the full range of the input is shown.","examples":["drawing","dynamic-update","is-zoomed-ignore-programmatic-zoom","no-visibility","reverse-y-axis","synchronize","zoom","two-axes-vr"]},{"name":"drawXAxis","tags":["axes","x-axis","display"],"type":"Boolean","default":true,"desc":"Whether to draw the x-axis. Setting this to false also prevents x-axis ticks from being drawn and reclaims the space for the chart grid/lines.","examples":["unboxed-spark"]},{"name":"drawYAxis","tags":["axes","y-axis","display"],"type":"Boolean","default":true,"desc":"Whether to draw the y-axis. Setting this to false also prevents y-axis ticks from being drawn and reclaims the space for the chart grid/lines.","examples":["drawing","unboxed-spark"]},{"name":"avoidMinZero","tags":["axes","y-axis"],"type":"Boolean","default":false,"desc":"When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis.","examples":["avoidMinZero"]},{"name":"includeZero","tags":["axes","y-axis","display"],"type":"Boolean","default":false,"desc":"Usually, dygraphs will use the range of the data plus some padding to set the range of the y-axis. If this option is set, the y-axis will always include zero, typically as the lowest value. This can be used to avoid exaggerating the variance in the data","examples":["no-range","numeric-gviz","small-range-zero"]},{"name":"axisLineWidth","tags":["axes","stroke","display"],"type":"Float","default":0.3,"desc":"Thickness (in pixels) of the x- and y-axis lines."},{"name":"axisLineColor","tags":["axes","color"],"type":"String","default":"black","desc":"Color of the x- and y-axis lines. Accepts any value which the HTML canvas strokeStyle attribute understands, e.g. 'black' or 'rgb(0, 100, 255)'.","examples":["demo"]},{"name":"axisTickSize","tags":["axes"],"type":"Float","default":3,"desc":"The size of the line to display next to each tick mark on x- or y-axes."},{"name":"xAxisHeight","tags":["axes","x-axis","display"],"type":"Integer","default":null,"desc":"Height, in pixels, of the x-axis. If not set explicitly, this is computed based on axisLabelFontSize and axisTickSize."},{"name":"axisLabelColor","tags":["axes","color","labels"],"type":"String","default":"black","desc":"Color for x- and y-axis labels. This is a CSS color string."},{"name":"axisLabelFontSize","tags":["axes","labels","fonts","standard"],"type":"Integer","default":14,"desc":"Size of the font (in pixels) to use in the axis labels, both x- and y-axis."},{"name":"axisLabelFormatter","tags":["formatting","axes","labels"],"type":"function(number or Date, granularity, opts, dygraph)","default":null,"desc":"Function to call to format the tick values that appear along an axis. This is usually set on a per-axis basis. The first parameter is either a number (for a numeric axis) or a Date object (for a date axis). The second argument specifies how fine-grained the axis is. For date axes, this is a reference to the time granularity enumeration, defined in dygraph-tickers.js, e.g. Dygraph.WEEKLY. opts is a function which provides access to various options on the dygraph, e.g. opts('labelsKMB').","examples":["x-axis-formatter","y-axis-formatter","value-axis-formatters"]},{"name":"axisLabelWidth","tags":["axes","labels"],"type":"Integer","default":50,"desc":"Width (in pixels) of the containing divs for x- and y-axis labels. For the y-axis, this also controls"},{"name":"xLabelHeight","tags":["labels","axes","x-axis","display"],"type":"Integer","default":18,"desc":"Height of the x-axis label, in pixels. This also controls the default font size of the x-axis label. If you style the label on your own, this controls how much space is set aside below the chart for the x-axis label's div."},{"name":"yLabelWidth","tags":["labels","axes","y-axis","display"],"type":"Integer","default":18,"desc":"Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."},{"name":"pixelsPerLabel","tags":["axes","grid","labels"],"type":"Integer","default":null,"desc":"Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks. This is set on a per-axis basis. By default, values are 60 (x-axis) or 30 (y-axes).","examples":["value-axis-formatters"]},{"name":"xAxisLabelWidth","tags":["axes","x-axis","labels"],"type":"Integer","default":50,"desc":"Width, in pixels, of the x-axis labels.","examples":["x-axis-formatter","value-axis-formatters"]},{"name":"yAxisLabelWidth","tags":["axes","y-axis","labels"],"type":"Integer","default":50,"desc":"Width, in pixels, of the y-axis labels. This also affects the amount of space available for a y-axis chart label.","examples":["customLabel","two-axes","multi-scale","two-axes-vr","value-axis-formatters"]},{"name":"ticker","tags":["axes","formatting","callback"],"type":"function(min, max, pixels, opts, dygraph, vals) -> [{v: ..., label: ...},...]","default":null,"desc":"This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result. By default, uses Dygraph.dateTicker or Dygraph.numericTicks, but see dygraph-tickers.js for an extensive discussion. This is set on a per-axis basis."},{"name":"labelsKMB","tags":["formatting","labels","y-axis","display"],"type":"Boolean","default":false,"desc":"Show K/M/B for thousands/millions/billions on y-axis.","examples":["annotation-gviz","century-scale","demo","labelsKMB","no-range","two-axes","reverse-y-axis","two-axes-vr"]},{"name":"labelsKMG2","tags":["formatting","labels","y-axis","display"],"type":"Boolean","default":false,"desc":"Show k/M/G for kilo/Mega/Giga on y-axis. This is different than labelsKMB in that it uses base 2, not 10.","examples":["labels","formatting"]},{"name":"digitsAfterDecimal","tags":["formatting","y-axis","display"],"type":"Integer","default":2,"desc":"Unless it's run in scientific mode (see the sigFigs option), dygraphs displays numbers with digitsAfterDecimal digits after the decimal point. Trailing zeros are not displayed, so with a value of 2 you'll get '0', '0.1', '0.12', '123.45' but not '123.456' (it will be rounded to '123.46'). Numbers with absolute value less than 0.1^digitsAfterDecimal (i.e. those which would show up as '0.00') will be displayed in scientific notation.","examples":["number-display"]},{"name":"maxNumberWidth","tags":["formatting","y-axis","display"],"type":"Integer","default":6,"desc":"When displaying numbers in normal (not scientific) mode, large numbers will be displayed with many trailing zeros (e.g. 100000000 instead of 1e9). This can lead to unwieldy y-axis labels. If there are more than maxNumberWidth digits to the left of the decimal in a number, dygraphs will switch to scientific notation, even when not operating in scientific mode. If you'd like to see all those digits, set this to something large, like 20 or 30.","examples":["number-display"]},{"name":"sigFigs","tags":["formatting","y-axis","display"],"type":"Integer","default":null,"desc":"By default, dygraphs displays numbers with a fixed number of digits after the decimal point. If you'd prefer to have a fixed number of significant figures, set this option to that number of sig figs. A value of 2, for instance, would cause 1 to be display as 1.0 and 1234 to be displayed as 1.23e+3.","examples":["number-display"]},{"name":"valueFormatter","tags":["formatting","callback","labels","display","legend"],"type":"function(num or millis, opts, dygraph)","default":null,"desc":"Function to provide a custom display format for the values displayed on mouseover. This does not affect the values that appear on tick marks next to the axes. To format those, see `axisLabelFormatter`. This is usually set on a per-axis basis. For date axes, you can call new Date(millis) to get a Date object. opts is a function you can call to access various options (e.g. opts('labelsKMB')). Default formatter will depend on the type of your data.","examples":["y-axis-formatter","value-axis-formatters"]},{"name":"title","ignore":true,"tags":["labels","title","display","chart","standard"],"type":"String","default":null,"desc":"Text to display above the chart. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-title' classes.","examples":["border","demo","noise","styled-chart-labels","multi-scale","range-selector","temperature-sf-ny"]},{"name":"titleHeight","ignore":true,"tags":["labels","display","title","fonts","chart"],"type":"Integer","default":18,"desc":"Height of the chart title, in pixels. This also controls the default font size of the title. If you style the title on your own, this controls how much space is set aside above the chart for the title's div.","examples":["styled-chart-labels"]},{"name":"showRoller","tags":["statistics","interactivity","rolling averages","summary data"],"type":"Boolean","default":false,"desc":"If the rolling average period text box should be shown.","examples":["annotation","callback","crosshair","dynamic-update","fractions","isolated-points","missing-data","numeric-gviz","steps","underlay-callback","range-selector","temperature-sf-ny"]},{"name":"rollPeriod","tags":["statistics","rolling averages","error bars","summary data"],"type":"Integer >= 1","default":1,"desc":"Number of days over which to average data. Discussed extensively above.","examples":["annotation","callback","century-scale","crosshair","customLabel","draw-points","dygraph-many-points-benchmark","grid_dot","link-interaction","missing-data","resize","no-visibility","noise","perf","reverse-y-axis","unboxed-spark","spacing","styled-chart-labels","synchronize","two-series","underlay-callback","visibility","range-selector","temperature-sf-ny"]},{"name":"sigma","tags":["statistics","error bars","summary data"],"type":"Float","default":2,"desc":"When errorBars is set, shade this many standard deviations above/below each point."},{"name":"wilsonInterval","tags":["statistics","error bars","summary data"],"type":"Boolean","default":true,"desc":"Use in conjunction with the \"fractions\" option. Instead of plotting +/- N standard deviations, dygraphs will compute a Wilson confidence interval and plot that. This has more reasonable behavior for ratios close to 0 or 1."},{"name":"fillAlpha","tags":["statistics","error bars","color","data","display","summary data"],"type":"Float (0.0 - 1.0)","default":0.15,"desc":"Error bars (or custom bars) for each series are drawn in the same color as the series, but with partial transparency. This sets the transparency. A value of 0.0 means that the error bars will not be drawn, whereas a value of 1.0 means that the error bars will be as dark as the line for the series itself. This can be used to produce chart lines whose thickness varies at each point."},{"name":"customBars","tags":["statistics","source","error bars","csv","parsing","summary data"],"type":"Boolean","default":false,"desc":"When set, parse each CSV cell as \"low;middle;high\". Error bars will be drawn for each point between low and high, with the series itself going through middle.","examples":["custom-bars","zero-series","stock","range-selector","temperature-sf-ny"]},{"name":"errorBars","tags":["statistics","source","error bars","csv","parsing","summary data"],"type":"Boolean","default":false,"desc":"Does the data contain standard deviations? Setting this to true alters the input format (see above).","examples":["callback","crosshair","custom-bars","customLabel","draw-points","fillGraph","fractions","grid_dot","interaction","is-zoomed-ignore-programmatic-zoom","link-interaction","linear-regression-fractions","missing-data","resize","no-visibility","noise","numeric-gviz","perf","steps","synchronize","underlay-callback","visibility","zoom"]},{"name":"fractions","tags":["statistics","source","error bars","csv","parsing","summary data"],"type":"Boolean","default":false,"desc":"When set, attempt to parse each cell in the CSV file as \"a/b\", where a and b are integers. The ratio will be plotted. This allows computation of Wilson confidence intervals (see below).","examples":["fractions","linear-regression-fractions"]},{"name":"delimiter","tags":["source","csv"],"type":"String","default":",","desc":"The delimiter to look for when separating fields of a CSV file. Setting this to a tab is not usually necessary, since tab-delimited data is auto-detected."},{"name":"xValueParser","tags":["source","csv","x-axis"],"type":"function(str) -> number","default":null,"desc":"A function which parses x-values (i.e. the dependent series). Must returna number, even when the values are dates. In this case, millis since epochare used. This is used primarily for parsing CSV data. \n\n * Dygraphs isslightly more accepting in the dates which it will parse."},{"name":"drawXGrid","tags":["grid","x-axis","standard"],"type":"Boolean","default":true,"desc":"Whether to display vertical gridlines under the chart.","examples":["demo","unboxed-spark"]},{"name":"drawYGrid","tags":["grid","y-axis","standard"],"type":"Boolean","default":true,"desc":"Whether to display horizontal gridlines under the chart.","examples":["drawing","unboxed-spark"]},{"name":"gridLineColor","tags":["grid","color","lines","graph"],"type":"String","default":"rgb(128,128,128)","desc":"The color of the gridlines. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\".","examples":["drawing","grid_dot"]},{"name":"gridLineWidth","tags":["grid","stroke","graph"],"type":"Float","default":0.3,"desc":"Thickness (in pixels) of the gridlines drawn under the chart. The vertical/horizontal gridlines can be turned off entirely by using the drawXGrid and drawYGrid options."},{"name":"showRangeSelector","tags":["range","interactivity","graph"],"type":"Boolean","default":false,"desc":"Show the range selector widget. This option can only be specified at Dygraph creation time.","examples":["range-selector"]},{"name":"rangeSelectorHeight","tags":["range","interactivity","graph"],"type":"Integer","default":40,"desc":"Height, in pixels, of the range selector widget. This option can only be specified at Dygraph creation time.","examples":["range-selector"]},{"name":"rangeSelectorPlotFillColor","tags":["range","interactivity","color","graph"],"type":"String","default":"#A7B1C4","desc":"The range selector mini plot fill color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off fill.","examples":["range-selector"]},{"name":"rangeSelectorPlotStrokeColor","tags":["range","interactivity","color","graph"],"type":"String","default":"#808FAB","desc":"The range selector mini plot stroke color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off stroke.","examples":["range-selector"]},{"name":"panEdgeFraction","tags":["interactivity","axes","pan","graph"],"type":"Float","default":null,"desc":"A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds.","examples":["zoom"]},{"name":"displayAnnotations","tags":["annotations","display","data"],"type":"Boolean","default":false,"desc":"Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart.","examples":["annotation-gviz"]},{"name":"annotationClickHandler","tags":["annotations","callback"],"type":"function(annotation, point, dygraph, event)","default":null,"desc":"If provided, this function is called whenever the user clicks on an annotation.","examples":["annotation"]},{"name":"annotationDblClickHandler","tags":["annotations","callback"],"type":"function(annotation, point, dygraph, event)","default":null,"desc":"If provided, this function is called whenever the user double-clicks on an annotation.","examples":["annotation"]},{"name":"annotationMouseOutHandler","tags":["annotations","callback"],"type":"function(annotation, point, dygraph, event)","default":null,"desc":"If provided, this function is called whenever the user mouses out of an annotation.","examples":["annotation"]},{"name":"annotationMouseOverHandler","tags":["annotations","callback"],"type":"function(annotation, point, dygraph, event)","default":null,"desc":"If provided, this function is called whenever the user mouses over an annotation.","examples":["annotation"]},{"name":"timingName","tags":["debugging"],"type":"String","default":null,"desc":"Set this option to log timing information. The value of the option will be logged along with the timing, so that you can distinguish multiple dygraphs on the same page.","examples":["dygraph-many-points-benchmark"]},{"name":"clickCallback","tags":["callback"],"type":"function(e, x, points)","default":null,"desc":"A function to call when the canvas is clicked. The function should take three arguments, the event object for the click, the x-value that was clicked (for dates this is millis since epoch), and the closest points along that date. The points have these properties:\n * xval/yval: The data coordinates of the point (with dates/times as millis since epoch) \n * canvasx/canvasy: The canvas coordinates at which the point is drawn. \n name: The name of the data series to which the point belongs","examples":["callback"]},{"name":"drawCallback","tags":["callback"],"type":"function(dygraph, is_initial)","default":null,"desc":"When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning. The first parameter is the dygraph being drawn. The second is a boolean value indicating whether this is the initial draw.","examples":["linear-regression-addseries","annotation","callback","is-zoomed","is-zoomed-ignore-programmatic-zoom","synchronize","zoom"]},{"name":"highlightCallback","tags":["callback"],"type":"function(event, x, points,row)","default":null,"desc":"When set, this callback gets called every time a new point is highlighted.The parameters are the JavaScript mousemove event, the x-coordinate of thehighlighted points and an array of highlighted points: [ {name: 'series',yval: y-value}, ... ]","examples":["callback","crosshair"]},{"name":"underlayCallback","tags":["callback"],"type":"function(canvas, area, dygraph)","default":null,"desc":"When set, this callback gets called before the chart is drawn. It details on how to use this.","examples":["highlighted-region","interaction","linear-regression-fractions","linear-regression","underlay-callback"]},{"name":"unhighlightCallback","tags":["callback"],"type":"function(event)","default":null,"desc":"When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph.  The parameter is the mouseout event.","examples":["callback","crosshair"]},{"name":"zoomCallback","tags":["callback","interactivity","zoom"],"type":"function(minDate, maxDate, yRanges)","default":null,"desc":"A function to call when the zoom window is changed (either by zooming in or out). minDate and maxDate are milliseconds since epoch. yRanges is an array of [bottom, top] pairs, one for each y-axis.","examples":["callback","is-zoomed-ignore-programmatic-zoom","zoom"]},{"name":"pointClickCallback","tags":["callback","interactivity"],"type":"function(e, point)","default":null,"desc":"A function to call when a data point is clicked. The function should taketwo arguments, the event object for the click, and the point that was clicked.The 'point' argument has these properties:\n * xval/yval: The data coordinatesof the point (with dates/times as millis since epoch) \n * canvasx/canvasy:The canvas coordinates at which the point is drawn. \n * name: The name ofthe data series to which the point belongs.","examples":["annotation","callback"]},{"name":"pixelsPerXLabel","tags":["deprecated","labels","x-axis"],"type":"Integer","default":null,"desc":"Prefer axes { x: { pixelsPerLabel } }"},{"name":"pixelsPerYLabel","tags":["deprecated","labels","y-axis"],"type":"Integer","default":null,"desc":"Prefer axes: { y: { pixelsPerLabel } }","examples":["spacing"]},{"name":"xAxisLabelFormatter","tags":["deprecated","labels","x-axis"],"type":"function","default":null,"desc":"Prefer axes { x: { axisLabelFormatter } }"},{"name":"xValueFormatter","tags":["deprecated","x-axis"],"type":"function","default":null,"desc":"Prefer axes: { x: { valueFormatter } }"},{"name":"yAxisLabelFormatter","tags":["deprecated","labels","y-axis"],"type":"function","default":null,"desc":"Prefer axes: { y: { axisLabelFormatter } }"},{"name":"yValueFormatter","tags":["deprecated","y-axis"],"type":"function","default":null,"desc":"Prefer axes: { y: { valueFormatter } }","examples":["labelsKMB","multi-scale"]},{"name":"width","ignore":true,"tags":["display","graph"],"type":"Integer","default":480,"desc":"Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."},{"name":"height","ignore":true,"tags":["display","graph"],"type":"Integer","default":320,"desc":"Height, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."},{"name":"file","ignore":true,"tags":["source","data"],"type":"String","default":null,"desc":"Sets the data being displayed in the chart. This can only be set when calling updateOptions; it cannot be set from the constructor. For a full description of valid data formats, see the Data Formats page. String can be a URL of CSV or CSV, GViz DataTable or 2D Array."}]
\ No newline at end of file