From 6d66c7a2b381fa68a27ab5e9ae5de373cb7ea08a Mon Sep 17 00:00:00 2001 From: David Schoonover Date: Tue, 10 Jul 2012 06:19:39 -0700 Subject: [PATCH] Adds compiled JS source files to be published with the npm distribution. --- lib/app.js | 58 ++ lib/app.mod.js | 62 ++ lib/base/base-mixin.js | 216 +++++++ lib/base/base-mixin.mod.js | 220 +++++++ lib/base/base-model.js | 254 ++++++++ lib/base/base-model.mod.js | 258 ++++++++ lib/base/base-view.js | 330 +++++++++++ lib/base/base-view.mod.js | 334 +++++++++++ lib/base/base.js | 77 +++ lib/base/base.mod.js | 81 +++ lib/base/cascading-model.js | 54 ++ lib/base/cascading-model.mod.js | 58 ++ lib/base/data-binding.js | 65 ++ lib/base/data-binding.mod.js | 69 +++ lib/base/index.js | 14 + lib/base/index.mod.js | 18 + lib/base/model-cache.js | 218 +++++++ lib/base/model-cache.mod.js | 222 +++++++ lib/base/scaffold/index.js | 9 + lib/base/scaffold/index.mod.js | 13 + lib/base/scaffold/scaffold-model.js | 131 ++++ lib/base/scaffold/scaffold-model.mod.js | 135 +++++ lib/base/scaffold/scaffold-view.js | 116 ++++ lib/base/scaffold/scaffold-view.mod.js | 120 ++++ lib/chart/chart-type.js | 435 ++++++++++++++ lib/chart/chart-type.mod.js | 439 ++++++++++++++ lib/chart/index.js | 12 + lib/chart/index.mod.js | 16 + lib/chart/option/chart-option-model.js | 256 ++++++++ lib/chart/option/chart-option-model.mod.js | 260 ++++++++ lib/chart/option/chart-option-view.js | 256 ++++++++ lib/chart/option/chart-option-view.mod.js | 260 ++++++++ lib/chart/option/index.js | 9 + lib/chart/option/index.mod.js | 13 + lib/chart/type/d3-chart.js | 92 +++ lib/chart/type/d3-chart.mod.js | 96 +++ lib/chart/type/d3/d3-bar-element.js | 51 ++ lib/chart/type/d3/d3-bar-element.mod.js | 55 ++ lib/chart/type/d3/d3-chart-element.js | 91 +++ lib/chart/type/d3/d3-chart-element.mod.js | 95 +++ lib/chart/type/d3/d3-line-element.js | 59 ++ lib/chart/type/d3/d3-line-element.mod.js | 63 ++ lib/chart/type/d3/index.js | 10 + lib/chart/type/d3/index.mod.js | 14 + lib/chart/type/dygraphs.js | 127 ++++ lib/chart/type/dygraphs.mod.js | 131 ++++ lib/chart/type/index.mod.js | 5 + lib/dashboard/dashboard-model.js | 87 +++ lib/dashboard/dashboard-model.mod.js | 91 +++ lib/dashboard/dashboard-view.js | 148 +++++ lib/dashboard/dashboard-view.mod.js | 152 +++++ lib/dashboard/index.js | 9 + lib/dashboard/index.mod.js | 13 + lib/data/data-view.js | 142 +++++ lib/data/data-view.mod.js | 146 +++++ lib/data/dataset-model.js | 184 ++++++ lib/data/dataset-model.mod.js | 188 ++++++ lib/data/dataset-view.js | 131 ++++ lib/data/dataset-view.mod.js | 135 +++++ lib/data/datasource-model.js | 217 +++++++ lib/data/datasource-model.mod.js | 221 +++++++ lib/data/datasource-ui-view.js | 76 +++ lib/data/datasource-ui-view.mod.js | 80 +++ lib/data/datasource-view.js | 21 + lib/data/datasource-view.mod.js | 25 + lib/data/index.js | 15 + lib/data/index.mod.js | 19 + lib/data/metric-edit-view.js | 81 +++ lib/data/metric-edit-view.mod.js | 85 +++ lib/data/metric-model.js | 180 ++++++ lib/data/metric-model.mod.js | 184 ++++++ lib/data/project-colors.js | 48 ++ lib/data/project-colors.mod.js | 52 ++ lib/graph/graph-display-view.js | 67 +++ lib/graph/graph-display-view.mod.js | 71 +++ lib/graph/graph-edit-view.js | 194 ++++++ lib/graph/graph-edit-view.mod.js | 198 ++++++ lib/graph/graph-list-view.js | 30 + lib/graph/graph-list-view.mod.js | 34 ++ lib/graph/graph-model.js | 447 ++++++++++++++ lib/graph/graph-model.mod.js | 451 ++++++++++++++ lib/graph/graph-view.js | 311 ++++++++++ lib/graph/graph-view.mod.js | 315 ++++++++++ lib/graph/index.js | 12 + lib/graph/index.mod.js | 16 + lib/main-dashboard.js | 30 + lib/main-display.js | 32 + lib/main-edit.js | 33 + lib/main-geo.js | 102 ++++ lib/main-graph-list.js | 23 + lib/template/browser-helpers.jade.js | 13 + lib/template/chart/chart-option.jade.js | 107 ++++ lib/template/chart/chart-option.jade.mod.js | 111 ++++ lib/template/chart/chart-scaffold.jade.js | 28 + lib/template/chart/chart-scaffold.jade.mod.js | 32 + lib/template/dashboard/dashboard-tab.jade.js | 14 + lib/template/dashboard/dashboard-tab.jade.mod.js | 18 + lib/template/dashboard/dashboard.jade.js | 24 + lib/template/dashboard/dashboard.jade.mod.js | 28 + lib/template/data/data.jade.js | 20 + lib/template/data/data.jade.mod.js | 24 + lib/template/data/dataset-metric.jade.js | 28 + lib/template/data/dataset-metric.jade.mod.js | 32 + lib/template/data/dataset.jade.js | 38 ++ lib/template/data/dataset.jade.mod.js | 42 ++ lib/template/data/datasource-ui.jade.js | 232 ++++++++ lib/template/data/datasource-ui.jade.mod.js | 236 ++++++++ lib/template/data/datasource.jade.js | 87 +++ lib/template/data/datasource.jade.mod.js | 91 +++ lib/template/data/metric-edit.jade.js | 40 ++ lib/template/data/metric-edit.jade.mod.js | 44 ++ lib/template/graph/graph-display.jade.js | 74 +++ lib/template/graph/graph-display.jade.mod.js | 78 +++ lib/template/graph/graph-edit.jade.js | 115 ++++ lib/template/graph/graph-edit.jade.mod.js | 119 ++++ lib/template/graph/graph-list.jade.js | 38 ++ lib/template/graph/graph-list.jade.mod.js | 42 ++ lib/util/backbone.js | 130 ++++ lib/util/backbone.mod.js | 134 +++++ lib/util/cascade.js | 417 +++++++++++++ lib/util/cascade.mod.js | 421 +++++++++++++ lib/util/event/index.js | 2 + lib/util/event/index.mod.js | 6 + lib/util/event/ready-emitter.js | 72 +++ lib/util/event/ready-emitter.mod.js | 76 +++ lib/util/event/waiting-emitter.js | 63 ++ lib/util/event/waiting-emitter.mod.js | 67 +++ lib/util/formatters.js | 71 +++ lib/util/formatters.mod.js | 75 +++ lib/util/index.js | 43 ++ lib/util/index.mod.js | 47 ++ lib/util/op.js | 291 +++++++++ lib/util/op.mod.js | 295 +++++++++ lib/util/parser.js | 173 ++++++ lib/util/parser.mod.js | 177 ++++++ lib/util/timeseries/csv.js | 133 +++++ lib/util/timeseries/csv.mod.js | 137 +++++ lib/util/timeseries/index.js | 2 + lib/util/timeseries/index.mod.js | 6 + lib/util/timeseries/timeseries.js | 199 +++++++ lib/util/timeseries/timeseries.mod.js | 203 +++++++ lib/util/underscore/array.js | 58 ++ lib/util/underscore/array.mod.js | 62 ++ lib/util/underscore/class.js | 73 +++ lib/util/underscore/class.mod.js | 77 +++ lib/util/underscore/function.js | 34 ++ lib/util/underscore/function.mod.js | 38 ++ lib/util/underscore/index.js | 31 + lib/util/underscore/index.mod.js | 35 ++ lib/util/underscore/kv.js | 74 +++ lib/util/underscore/kv.mod.js | 78 +++ lib/util/underscore/object.js | 301 ++++++++++ lib/util/underscore/object.mod.js | 305 ++++++++++ lib/util/underscore/string.js | 101 ++++ lib/util/underscore/string.mod.js | 105 ++++ www/css/chart.css | 12 + www/css/colors.css | 5 + www/css/dashboard.css | 28 + www/css/data.css | 328 ++++++++++ www/css/docs.css | 40 ++ www/css/geo-display.css | 88 +++ www/css/graph-display-print.css | 9 + www/css/graph-display.css | 648 ++++++++++++++++++++ www/css/graph.css | 691 ++++++++++++++++++++++ www/css/hicons.css | 106 ++++ www/css/layout.css | 422 +++++++++++++ www/css/mixins.css | 48 ++ www/css/text.css | 48 ++ www/schema/d3/d3-bar.json | 1 + www/schema/d3/d3-chart.json | 1 + www/schema/d3/d3-geo-world.json | 1 + www/schema/d3/d3-line.json | 1 + www/schema/dygraph.json | 1 + 173 files changed, 19918 insertions(+), 0 deletions(-) create mode 100644 lib/app.js create mode 100644 lib/app.mod.js create mode 100644 lib/base/base-mixin.js create mode 100644 lib/base/base-mixin.mod.js create mode 100644 lib/base/base-model.js create mode 100644 lib/base/base-model.mod.js create mode 100644 lib/base/base-view.js create mode 100644 lib/base/base-view.mod.js create mode 100644 lib/base/base.js create mode 100644 lib/base/base.mod.js create mode 100644 lib/base/cascading-model.js create mode 100644 lib/base/cascading-model.mod.js create mode 100644 lib/base/data-binding.js create mode 100644 lib/base/data-binding.mod.js create mode 100644 lib/base/index.js create mode 100644 lib/base/index.mod.js create mode 100644 lib/base/model-cache.js create mode 100644 lib/base/model-cache.mod.js create mode 100644 lib/base/scaffold/index.js create mode 100644 lib/base/scaffold/index.mod.js create mode 100644 lib/base/scaffold/scaffold-model.js create mode 100644 lib/base/scaffold/scaffold-model.mod.js create mode 100644 lib/base/scaffold/scaffold-view.js create mode 100644 lib/base/scaffold/scaffold-view.mod.js create mode 100644 lib/chart/chart-type.js create mode 100644 lib/chart/chart-type.mod.js create mode 100644 lib/chart/index.js create mode 100644 lib/chart/index.mod.js create mode 100644 lib/chart/option/chart-option-model.js create mode 100644 lib/chart/option/chart-option-model.mod.js create mode 100644 lib/chart/option/chart-option-view.js create mode 100644 lib/chart/option/chart-option-view.mod.js create mode 100644 lib/chart/option/index.js create mode 100644 lib/chart/option/index.mod.js create mode 100644 lib/chart/type/d3-chart.js create mode 100644 lib/chart/type/d3-chart.mod.js create mode 100644 lib/chart/type/d3/d3-bar-element.js create mode 100644 lib/chart/type/d3/d3-bar-element.mod.js create mode 100644 lib/chart/type/d3/d3-chart-element.js create mode 100644 lib/chart/type/d3/d3-chart-element.mod.js create mode 100644 lib/chart/type/d3/d3-line-element.js create mode 100644 lib/chart/type/d3/d3-line-element.mod.js create mode 100644 lib/chart/type/d3/index.js create mode 100644 lib/chart/type/d3/index.mod.js create mode 100644 lib/chart/type/dygraphs.js create mode 100644 lib/chart/type/dygraphs.mod.js create mode 100644 lib/chart/type/index.js create mode 100644 lib/chart/type/index.mod.js create mode 100644 lib/dashboard/dashboard-model.js create mode 100644 lib/dashboard/dashboard-model.mod.js create mode 100644 lib/dashboard/dashboard-view.js create mode 100644 lib/dashboard/dashboard-view.mod.js create mode 100644 lib/dashboard/index.js create mode 100644 lib/dashboard/index.mod.js create mode 100644 lib/data/data-view.js create mode 100644 lib/data/data-view.mod.js create mode 100644 lib/data/dataset-model.js create mode 100644 lib/data/dataset-model.mod.js create mode 100644 lib/data/dataset-view.js create mode 100644 lib/data/dataset-view.mod.js create mode 100644 lib/data/datasource-model.js create mode 100644 lib/data/datasource-model.mod.js create mode 100644 lib/data/datasource-ui-view.js create mode 100644 lib/data/datasource-ui-view.mod.js create mode 100644 lib/data/datasource-view.js create mode 100644 lib/data/datasource-view.mod.js create mode 100644 lib/data/index.js create mode 100644 lib/data/index.mod.js create mode 100644 lib/data/metric-edit-view.js create mode 100644 lib/data/metric-edit-view.mod.js create mode 100644 lib/data/metric-model.js create mode 100644 lib/data/metric-model.mod.js create mode 100644 lib/data/project-colors.js create mode 100644 lib/data/project-colors.mod.js create mode 100644 lib/graph/graph-display-view.js create mode 100644 lib/graph/graph-display-view.mod.js create mode 100644 lib/graph/graph-edit-view.js create mode 100644 lib/graph/graph-edit-view.mod.js create mode 100644 lib/graph/graph-list-view.js create mode 100644 lib/graph/graph-list-view.mod.js create mode 100644 lib/graph/graph-model.js create mode 100644 lib/graph/graph-model.mod.js create mode 100644 lib/graph/graph-view.js create mode 100644 lib/graph/graph-view.mod.js create mode 100644 lib/graph/index.js create mode 100644 lib/graph/index.mod.js create mode 100644 lib/main-dashboard.js create mode 100644 lib/main-display.js create mode 100644 lib/main-edit.js create mode 100644 lib/main-geo.js create mode 100644 lib/main-graph-list.js create mode 100644 lib/template/browser-helpers.jade.js create mode 100644 lib/template/chart/chart-option.jade.js create mode 100644 lib/template/chart/chart-option.jade.mod.js create mode 100644 lib/template/chart/chart-scaffold.jade.js create mode 100644 lib/template/chart/chart-scaffold.jade.mod.js create mode 100644 lib/template/dashboard/dashboard-tab.jade.js create mode 100644 lib/template/dashboard/dashboard-tab.jade.mod.js create mode 100644 lib/template/dashboard/dashboard.jade.js create mode 100644 lib/template/dashboard/dashboard.jade.mod.js create mode 100644 lib/template/data/data.jade.js create mode 100644 lib/template/data/data.jade.mod.js create mode 100644 lib/template/data/dataset-metric.jade.js create mode 100644 lib/template/data/dataset-metric.jade.mod.js create mode 100644 lib/template/data/dataset.jade.js create mode 100644 lib/template/data/dataset.jade.mod.js create mode 100644 lib/template/data/datasource-ui.jade.js create mode 100644 lib/template/data/datasource-ui.jade.mod.js create mode 100644 lib/template/data/datasource.jade.js create mode 100644 lib/template/data/datasource.jade.mod.js create mode 100644 lib/template/data/metric-edit.jade.js create mode 100644 lib/template/data/metric-edit.jade.mod.js create mode 100644 lib/template/graph/graph-display.jade.js create mode 100644 lib/template/graph/graph-display.jade.mod.js create mode 100644 lib/template/graph/graph-edit.jade.js create mode 100644 lib/template/graph/graph-edit.jade.mod.js create mode 100644 lib/template/graph/graph-list.jade.js create mode 100644 lib/template/graph/graph-list.jade.mod.js create mode 100644 lib/util/backbone.js create mode 100644 lib/util/backbone.mod.js create mode 100644 lib/util/cascade.js create mode 100644 lib/util/cascade.mod.js create mode 100644 lib/util/event/index.js create mode 100644 lib/util/event/index.mod.js create mode 100644 lib/util/event/ready-emitter.js create mode 100644 lib/util/event/ready-emitter.mod.js create mode 100644 lib/util/event/waiting-emitter.js create mode 100644 lib/util/event/waiting-emitter.mod.js create mode 100644 lib/util/formatters.js create mode 100644 lib/util/formatters.mod.js create mode 100644 lib/util/index.js create mode 100644 lib/util/index.mod.js create mode 100644 lib/util/op.js create mode 100644 lib/util/op.mod.js create mode 100644 lib/util/parser.js create mode 100644 lib/util/parser.mod.js create mode 100644 lib/util/timeseries/csv.js create mode 100644 lib/util/timeseries/csv.mod.js create mode 100644 lib/util/timeseries/index.js create mode 100644 lib/util/timeseries/index.mod.js create mode 100644 lib/util/timeseries/timeseries.js create mode 100644 lib/util/timeseries/timeseries.mod.js create mode 100644 lib/util/underscore/array.js create mode 100644 lib/util/underscore/array.mod.js create mode 100644 lib/util/underscore/class.js create mode 100644 lib/util/underscore/class.mod.js create mode 100644 lib/util/underscore/function.js create mode 100644 lib/util/underscore/function.mod.js create mode 100644 lib/util/underscore/index.js create mode 100644 lib/util/underscore/index.mod.js create mode 100644 lib/util/underscore/kv.js create mode 100644 lib/util/underscore/kv.mod.js create mode 100644 lib/util/underscore/object.js create mode 100644 lib/util/underscore/object.mod.js create mode 100644 lib/util/underscore/string.js create mode 100644 lib/util/underscore/string.mod.js create mode 100644 www/css/bootstrap-variables.css create mode 100644 www/css/chart.css create mode 100644 www/css/colors.css create mode 100644 www/css/dashboard.css create mode 100644 www/css/data.css create mode 100644 www/css/docs.css create mode 100644 www/css/geo-display.css create mode 100644 www/css/graph-display-print.css create mode 100644 www/css/graph-display.css create mode 100644 www/css/graph.css create mode 100644 www/css/hicons.css create mode 100644 www/css/layout.css create mode 100644 www/css/mixins.css create mode 100644 www/css/text.css create mode 100644 www/schema/d3/d3-bar.json create mode 100644 www/schema/d3/d3-chart.json create mode 100644 www/schema/d3/d3-geo-world.json create mode 100644 www/schema/d3/d3-line.json create mode 100644 www/schema/dygraph.json diff --git a/lib/app.js b/lib/app.js new file mode 100644 index 0000000..b4937f3 --- /dev/null +++ b/lib/app.js @@ -0,0 +1,58 @@ +var Backbone, op, AppView, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Application view, automatically attaching to an existing element + * found at `appSelector`. + * @extends Backbone.View + */ +AppView = exports.AppView = Backbone.View.extend({ + appSelector: '#content .inner' + /** + * @constructor + */, + constructor: (function(){ + function AppView(options){ + var that, _this = this; + options == null && (options = {}); + if (typeof options === 'function') { + this.initialize = options; + options = {}; + } else { + if (that = options.initialize) { + this.initialize = that; + } + } + if (that = options.appSelector) { + this.appSelector = that; + } + options.el || (options.el = jQuery(this.appSelector)[0]); + Backbone.View.call(this, options); + jQuery(function(){ + return _this.render(); + }); + return this; + } + return AppView; + }()) + /** + * Override to set up your app. This method may be passed + * as an option to the constructor. + */, + initialize: function(){} + /** + * Append subviews. + */, + render: function(){ + var _ref; + if (this.view && !((_ref = this.view.$el.parent()) != null && _ref.length)) { + return this.$el.append(this.view.el); + } + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}); \ No newline at end of file diff --git a/lib/app.mod.js b/lib/app.mod.js new file mode 100644 index 0000000..e816fc5 --- /dev/null +++ b/lib/app.mod.js @@ -0,0 +1,62 @@ +require.define('/node_modules/kraken/app.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, AppView, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Application view, automatically attaching to an existing element + * found at `appSelector`. + * @extends Backbone.View + */ +AppView = exports.AppView = Backbone.View.extend({ + appSelector: '#content .inner' + /** + * @constructor + */, + constructor: (function(){ + function AppView(options){ + var that, _this = this; + options == null && (options = {}); + if (typeof options === 'function') { + this.initialize = options; + options = {}; + } else { + if (that = options.initialize) { + this.initialize = that; + } + } + if (that = options.appSelector) { + this.appSelector = that; + } + options.el || (options.el = jQuery(this.appSelector)[0]); + Backbone.View.call(this, options); + jQuery(function(){ + return _this.render(); + }); + return this; + } + return AppView; + }()) + /** + * Override to set up your app. This method may be passed + * as an option to the constructor. + */, + initialize: function(){} + /** + * Append subviews. + */, + render: function(){ + var _ref; + if (this.view && !((_ref = this.view.$el.parent()) != null && _ref.length)) { + return this.$el.append(this.view.el); + } + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}); + +}); diff --git a/lib/base/base-mixin.js b/lib/base/base-mixin.js new file mode 100644 index 0000000..eba7b88 --- /dev/null +++ b/lib/base/base-mixin.js @@ -0,0 +1,216 @@ +var Backbone, op, BaseBackboneMixin, Mixin, mixinBase, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseBackboneMixin = exports.BaseBackboneMixin = { + initialize: function(){ + return this.__apply_bind__(); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */, + __bind__: [] + /** + * Applies the contents of `__bind__`. + */, + __apply_bind__: function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + } + /** + * Whether we're ready. + * @type Boolean + */, + ready: false + /** + * Triggers the 'ready' event if it has not yet been triggered. + * Subsequent listeners added on this event will be auto-triggered. + * @returns {this} + */, + triggerReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (this[lock]) { + return this; + } + this[lock] = true; + this.trigger(event, this); + return this; + } + /** + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @returns {this} + */, + resetReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (!this[lock]) { + return this; + } + this[lock] = false; + this.trigger(event + "-reset", this); + return this; + } + /** + * Wrap {@link Backbone.Event#on} registration to handle registrations + * on 'ready' after we've broadcast the event. Handler will always still + * be registered, however, in case the emitter is reset. + * + * @param {String} events Space-separated events for which to register. + * @param {Function} callback + * @param {Object} [context] + * @returns {this} + */, + on: function(events, callback, context){ + context == null && (context = this); + if (!callback) { + return this; + } + Backbone.Events.on.apply(this, arguments); + if (this.ready && _.contains(events.split(/\s+/), 'ready')) { + callback.call(context, this); + } + return this; + }, + makeHandlersForCallback: function(cb){ + var _this = this; + return { + success: function(){ + return cb.call(_this, [null].concat(arguments)); + }, + error: function(it){ + return cb.call(_this, it); + } + }; + } + /** + * Count of outstanding tasks. + * @type Number + */, + waitingOn: 0 + /** + * Increment the waiting task counter. + * @returns {this} + */, + wait: function(){ + var count; + count = this.waitingOn; + this.waitingOn += 1; + if (count === 0 && this.waitingOn > 0) { + this.trigger('start-waiting', this); + } + return this; + } + /** + * Decrement the waiting task counter. + * @returns {this} + */, + unwait: function(){ + var count; + count = this.waitingOn; + this.waitingOn -= 1; + if (this.waitingOn === 0 && count > 0) { + this.trigger('stop-waiting', this); + } + return this; + } + /** + * @param {Function} fn Function to wrap. + * @returns {Function} A function wrapping the passed function with a call + * to `unwait()`, then delegating with current context and arguments. + */, + unwaitAnd: function(fn){ + var self; + self = this; + return function(){ + self.unwait(); + return fn.apply(this, arguments); + }; + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}; +/** + * @class Base mixin class. Extend this to create a new mixin, attaching the + * donor methods as you would instance methods. + * + * To mingle your mixin with another class or object: + * + * class MyMixin extends Mixin + * foo: -> "foo!" + * + * # Mix into an object... + * o = MyMixin.mix { bar:1 } + * + * # Mix into a Coco class... + * class Bar + * MyMixin.mix this + * bar : 1 + * + */ +exports.Mixin = Mixin = (function(){ + /** + * Mixes this mixin into the target. If `target` is not a class, a new + * object will be returned which inherits from the mixin. + */ + Mixin.displayName = 'Mixin'; + var prototype = Mixin.prototype, constructor = Mixin; + Mixin.mix = function(target){ + var MixinClass; + if (!target) { + return that; + } + MixinClass = Mixin; + if (this instanceof Mixin) { + MixinClass = this.constructor; + } + if (this instanceof Function) { + MixinClass = this; + } + if (typeof target === 'function') { + __import(target.prototype, MixinClass.prototype); + } else { + target = __import(_.clone(MixinClass.prototype), target); + } + (target.__mixins__ || (target.__mixins__ = [])).push(MixinClass); + return target; + }; + /** + * Coco metaprogramming hook to propagate class properties and methods. + */ + Mixin.extended = function(SubClass){ + var SuperClass, k, v, _own = {}.hasOwnProperty; + SuperClass = this; + for (k in SuperClass) if (_own.call(SuperClass, k)) { + v = SuperClass[k]; + if (!SubClass[k]) { + SubClass[k] = v; + } + } + return SubClass; + }; + function Mixin(){} + return Mixin; +}()); +/** + * Mixes BaseBackboneMixin into another object or prototype. + * @returns {Object} The merged prototype object. + */ +mixinBase = exports.mixinBase = function(){ + var bodies; + bodies = __slice.call(arguments); + return _.extend.apply(_, [_.clone(BaseBackboneMixin)].concat(__slice.call(bodies))); +}; +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base-mixin.mod.js b/lib/base/base-mixin.mod.js new file mode 100644 index 0000000..e2c68e2 --- /dev/null +++ b/lib/base/base-mixin.mod.js @@ -0,0 +1,220 @@ +require.define('/node_modules/kraken/base/base-mixin.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, Mixin, mixinBase, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseBackboneMixin = exports.BaseBackboneMixin = { + initialize: function(){ + return this.__apply_bind__(); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */, + __bind__: [] + /** + * Applies the contents of `__bind__`. + */, + __apply_bind__: function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + } + /** + * Whether we're ready. + * @type Boolean + */, + ready: false + /** + * Triggers the 'ready' event if it has not yet been triggered. + * Subsequent listeners added on this event will be auto-triggered. + * @returns {this} + */, + triggerReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (this[lock]) { + return this; + } + this[lock] = true; + this.trigger(event, this); + return this; + } + /** + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @returns {this} + */, + resetReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (!this[lock]) { + return this; + } + this[lock] = false; + this.trigger(event + "-reset", this); + return this; + } + /** + * Wrap {@link Backbone.Event#on} registration to handle registrations + * on 'ready' after we've broadcast the event. Handler will always still + * be registered, however, in case the emitter is reset. + * + * @param {String} events Space-separated events for which to register. + * @param {Function} callback + * @param {Object} [context] + * @returns {this} + */, + on: function(events, callback, context){ + context == null && (context = this); + if (!callback) { + return this; + } + Backbone.Events.on.apply(this, arguments); + if (this.ready && _.contains(events.split(/\s+/), 'ready')) { + callback.call(context, this); + } + return this; + }, + makeHandlersForCallback: function(cb){ + var _this = this; + return { + success: function(){ + return cb.call(_this, [null].concat(arguments)); + }, + error: function(it){ + return cb.call(_this, it); + } + }; + } + /** + * Count of outstanding tasks. + * @type Number + */, + waitingOn: 0 + /** + * Increment the waiting task counter. + * @returns {this} + */, + wait: function(){ + var count; + count = this.waitingOn; + this.waitingOn += 1; + if (count === 0 && this.waitingOn > 0) { + this.trigger('start-waiting', this); + } + return this; + } + /** + * Decrement the waiting task counter. + * @returns {this} + */, + unwait: function(){ + var count; + count = this.waitingOn; + this.waitingOn -= 1; + if (this.waitingOn === 0 && count > 0) { + this.trigger('stop-waiting', this); + } + return this; + } + /** + * @param {Function} fn Function to wrap. + * @returns {Function} A function wrapping the passed function with a call + * to `unwait()`, then delegating with current context and arguments. + */, + unwaitAnd: function(fn){ + var self; + self = this; + return function(){ + self.unwait(); + return fn.apply(this, arguments); + }; + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}; +/** + * @class Base mixin class. Extend this to create a new mixin, attaching the + * donor methods as you would instance methods. + * + * To mingle your mixin with another class or object: + * + * class MyMixin extends Mixin + * foo: -> "foo!" + * + * # Mix into an object... + * o = MyMixin.mix { bar:1 } + * + * # Mix into a Coco class... + * class Bar + * MyMixin.mix this + * bar : 1 + * + */ +exports.Mixin = Mixin = (function(){ + /** + * Mixes this mixin into the target. If `target` is not a class, a new + * object will be returned which inherits from the mixin. + */ + Mixin.displayName = 'Mixin'; + var prototype = Mixin.prototype, constructor = Mixin; + Mixin.mix = function(target){ + var MixinClass; + if (!target) { + return that; + } + MixinClass = Mixin; + if (this instanceof Mixin) { + MixinClass = this.constructor; + } + if (this instanceof Function) { + MixinClass = this; + } + if (typeof target === 'function') { + __import(target.prototype, MixinClass.prototype); + } else { + target = __import(_.clone(MixinClass.prototype), target); + } + (target.__mixins__ || (target.__mixins__ = [])).push(MixinClass); + return target; + }; + /** + * Coco metaprogramming hook to propagate class properties and methods. + */ + Mixin.extended = function(SubClass){ + var SuperClass, k, v, _own = {}.hasOwnProperty; + SuperClass = this; + for (k in SuperClass) if (_own.call(SuperClass, k)) { + v = SuperClass[k]; + if (!SubClass[k]) { + SubClass[k] = v; + } + } + return SubClass; + }; + function Mixin(){} + return Mixin; +}()); +/** + * Mixes BaseBackboneMixin into another object or prototype. + * @returns {Object} The merged prototype object. + */ +mixinBase = exports.mixinBase = function(){ + var bodies; + bodies = __slice.call(arguments); + return _.extend.apply(_, [_.clone(BaseBackboneMixin)].concat(__slice.call(bodies))); +}; +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/base-model.js b/lib/base/base-model.js new file mode 100644 index 0000000..7a4a6d6 --- /dev/null +++ b/lib/base/base-model.js @@ -0,0 +1,254 @@ +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ +BaseModel = exports.BaseModel = Backbone.Model.extend(mixinBase({ + constructor: (function(){ + function BaseModel(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Model.apply(this, arguments); + } + return BaseModel; + }()), + url: function(){ + return this.urlRoot + "/" + this.get('id') + ".json"; + }, + has: function(key){ + return this.get(key) != null; + }, + get: function(key){ + return _.getNested(this.attributes, key); + } + /** + * Override to customize what data or assets the model requires, + * and how they should be loaded. + * + * By default, `load()` simply calls `loadModel()` via `loader()`. + * + * @see BaseModel#loader + * @see BaseModel#loadModel + * @returns {this} + */, + load: function(){ + console.log(this + ".load()"); + this.loader({ + start: this.loadModel, + completeEvent: 'fetch-success' + }); + return this; + } + /** + * Wraps the loading workflow boilerplate: + * - Squelches multiple loads from running at once + * - Squelches loads post-ready, unless forced + * - Triggers a start event + * - Triggers "ready" when complete + * - Wraps workflow with wait/unwait + * - Cleans up "loading" state + * + * @protected + * @param {Object} [opts={}] Options: + * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context. + * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load. + * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed successfully. + * @param {String} [opts.errorEvent='load-error'] Event which signals loading has completed but failed. + * @param {Boolean} [opts.force=false] If true, reset ready state if we're ready before proceeding. + * @param {Boolean} [opts.readyIfError=false] If true, move fire the ready event when loading completes, even if it failed. + * @returns {this} + */, + loader: function(opts){ + var _this = this; + opts == null && (opts = {}); + opts = (__import({ + force: false, + readyIfError: false, + startEvent: 'load', + completeEvent: 'load-success', + errorEvent: 'load-error' + }, opts)); + if (opts.force) { + this.resetReady(); + } + if (!opts.start) { + throw new Error('You must specify a `start` function to start loading!'); + } + if (this.loading || this.ready) { + return this; + } + this.wait(); + this.loading = true; + this.trigger(opts.startEvent, this); + this.once(opts.completeEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.completeEvent !== 'load-success') { + _this.trigger('load-success', _this); + } + return _this.triggerReady(); + }); + this.once(opts.errorEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.errorEvent !== 'load-error') { + _this.trigger('load-error', _this); + } + if (opts.readyIfError) { + return _this.triggerReady(); + } + }); + opts.start.call(this); + return this; + } + /** + * Runs `.fetch()`, triggering a `fetch` event at start, and + * `fetch-success` / `fetch-error` on completion. + * + * @protected + * @returns {this} + */, + loadModel: function(){ + var _this = this; + this.wait(); + this.trigger('fetch', this); + this.fetch({ + success: function(){ + _this.unwait(); + return _this.trigger('fetch-success', _this); + }, + error: function(){ + _this.unwait(); + return _this.trigger.apply(_this, ['fetch-error', _this].concat(__slice.call(arguments))); + } + }); + return this; + }, + serialize: function(v){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return String(v); + } + /** + * Like `.toJSON()` in that it should return a plain object with no functions, + * but for the purpose of `.toKV()`, allowing you to customize the values + * included and keys used. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepFunctions=false] If false, functions will be omitted from the result. + * @returns {Object} + */, + toKVPairs: function(opts){ + var kvo, k, v; + opts == null && (opts = {}); + opts = (__import({ + keepFunctions: false + }, opts)); + kvo = _.collapseObject(this.toJSON()); + for (k in kvo) { + v = kvo[k]; + if (opts.keepFunctions || typeof v !== 'function') { + kvo[k] = this.serialize(v); + } + } + return kvo; + } + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + } + /** + * @returns {String} URL identifying this model. + */, + toURL: function(){ + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "(cid=" + this.cid + ", id=" + this.id + ")"; + } +})); +__import(BaseModel, { + /** + * Factory method which constructs an instance of this model from a string of KV-pairs. + * This is a class method inherited by models which extend {BaseModel}. + * @static + * @param {String|Object} o Serialized KV-pairs (or a plain object). + * @returns {BaseModel} An instance of this model. + */ + fromKV: function(o, item_delim, kv_delim){ + var Cls; + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + if (typeof o === 'string') { + o = _.fromKV(o, item_delim, kv_delim); + } + Cls = typeof this === 'function' + ? this + : this.constructor; + return new Cls(_.uncollapseObject(o)); + } +}); +/** + * @class Base collection, extending Backbone.Collection, used by scaffold and others. + * @extends Backbone.Collection + */ +BaseList = exports.BaseList = Backbone.Collection.extend(mixinBase({ + constructor: (function(){ + function BaseList(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Collection.apply(this, arguments); + } + return BaseList; + }()), + getIds: function(){ + return this.models.map(function(it){ + return it.id || it.get('id') || it.cid; + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.toJSON()); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "[" + this.length + "]"; + }, + toStringWithIds: function(){ + var modelIds; + modelIds = this.models.map(function(it){ + var _ref; + return "\"" + ((_ref = it.id) != null + ? _ref + : it.cid) + "\""; + }).join(', '); + return this.getClassName() + "[" + this.length + "](" + modelIds + ")"; + } +})); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base-model.mod.js b/lib/base/base-model.mod.js new file mode 100644 index 0000000..1fdd53b --- /dev/null +++ b/lib/base/base-model.mod.js @@ -0,0 +1,258 @@ +require.define('/node_modules/kraken/base/base-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ +BaseModel = exports.BaseModel = Backbone.Model.extend(mixinBase({ + constructor: (function(){ + function BaseModel(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Model.apply(this, arguments); + } + return BaseModel; + }()), + url: function(){ + return this.urlRoot + "/" + this.get('id') + ".json"; + }, + has: function(key){ + return this.get(key) != null; + }, + get: function(key){ + return _.getNested(this.attributes, key); + } + /** + * Override to customize what data or assets the model requires, + * and how they should be loaded. + * + * By default, `load()` simply calls `loadModel()` via `loader()`. + * + * @see BaseModel#loader + * @see BaseModel#loadModel + * @returns {this} + */, + load: function(){ + console.log(this + ".load()"); + this.loader({ + start: this.loadModel, + completeEvent: 'fetch-success' + }); + return this; + } + /** + * Wraps the loading workflow boilerplate: + * - Squelches multiple loads from running at once + * - Squelches loads post-ready, unless forced + * - Triggers a start event + * - Triggers "ready" when complete + * - Wraps workflow with wait/unwait + * - Cleans up "loading" state + * + * @protected + * @param {Object} [opts={}] Options: + * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context. + * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load. + * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed successfully. + * @param {String} [opts.errorEvent='load-error'] Event which signals loading has completed but failed. + * @param {Boolean} [opts.force=false] If true, reset ready state if we're ready before proceeding. + * @param {Boolean} [opts.readyIfError=false] If true, move fire the ready event when loading completes, even if it failed. + * @returns {this} + */, + loader: function(opts){ + var _this = this; + opts == null && (opts = {}); + opts = (__import({ + force: false, + readyIfError: false, + startEvent: 'load', + completeEvent: 'load-success', + errorEvent: 'load-error' + }, opts)); + if (opts.force) { + this.resetReady(); + } + if (!opts.start) { + throw new Error('You must specify a `start` function to start loading!'); + } + if (this.loading || this.ready) { + return this; + } + this.wait(); + this.loading = true; + this.trigger(opts.startEvent, this); + this.once(opts.completeEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.completeEvent !== 'load-success') { + _this.trigger('load-success', _this); + } + return _this.triggerReady(); + }); + this.once(opts.errorEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.errorEvent !== 'load-error') { + _this.trigger('load-error', _this); + } + if (opts.readyIfError) { + return _this.triggerReady(); + } + }); + opts.start.call(this); + return this; + } + /** + * Runs `.fetch()`, triggering a `fetch` event at start, and + * `fetch-success` / `fetch-error` on completion. + * + * @protected + * @returns {this} + */, + loadModel: function(){ + var _this = this; + this.wait(); + this.trigger('fetch', this); + this.fetch({ + success: function(){ + _this.unwait(); + return _this.trigger('fetch-success', _this); + }, + error: function(){ + _this.unwait(); + return _this.trigger.apply(_this, ['fetch-error', _this].concat(__slice.call(arguments))); + } + }); + return this; + }, + serialize: function(v){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return String(v); + } + /** + * Like `.toJSON()` in that it should return a plain object with no functions, + * but for the purpose of `.toKV()`, allowing you to customize the values + * included and keys used. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepFunctions=false] If false, functions will be omitted from the result. + * @returns {Object} + */, + toKVPairs: function(opts){ + var kvo, k, v; + opts == null && (opts = {}); + opts = (__import({ + keepFunctions: false + }, opts)); + kvo = _.collapseObject(this.toJSON()); + for (k in kvo) { + v = kvo[k]; + if (opts.keepFunctions || typeof v !== 'function') { + kvo[k] = this.serialize(v); + } + } + return kvo; + } + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + } + /** + * @returns {String} URL identifying this model. + */, + toURL: function(){ + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "(cid=" + this.cid + ", id=" + this.id + ")"; + } +})); +__import(BaseModel, { + /** + * Factory method which constructs an instance of this model from a string of KV-pairs. + * This is a class method inherited by models which extend {BaseModel}. + * @static + * @param {String|Object} o Serialized KV-pairs (or a plain object). + * @returns {BaseModel} An instance of this model. + */ + fromKV: function(o, item_delim, kv_delim){ + var Cls; + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + if (typeof o === 'string') { + o = _.fromKV(o, item_delim, kv_delim); + } + Cls = typeof this === 'function' + ? this + : this.constructor; + return new Cls(_.uncollapseObject(o)); + } +}); +/** + * @class Base collection, extending Backbone.Collection, used by scaffold and others. + * @extends Backbone.Collection + */ +BaseList = exports.BaseList = Backbone.Collection.extend(mixinBase({ + constructor: (function(){ + function BaseList(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Collection.apply(this, arguments); + } + return BaseList; + }()), + getIds: function(){ + return this.models.map(function(it){ + return it.id || it.get('id') || it.cid; + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.toJSON()); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "[" + this.length + "]"; + }, + toStringWithIds: function(){ + var modelIds; + modelIds = this.models.map(function(it){ + var _ref; + return "\"" + ((_ref = it.id) != null + ? _ref + : it.cid) + "\""; + }).join(', '); + return this.getClassName() + "[" + this.length + "](" + modelIds + ")"; + } +})); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/base-view.js b/lib/base/base-view.js new file mode 100644 index 0000000..9dec625 --- /dev/null +++ b/lib/base/base-view.js @@ -0,0 +1,330 @@ +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, DataBinding, BaseView, ViewList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +BaseModel = require('kraken/base/base-mixin').BaseModel; +DataBinding = require('kraken/base/data-binding').DataBinding; +/** + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View + */ +BaseView = exports.BaseView = Backbone.View.extend(mixinBase({ + tagName: 'section', + model: BaseModel + /** + * Method-name called by `onReturnKeypress` when used as an event-handler. + * @type String + */, + callOnReturnKeypress: null + /** + * Parent view of this view. + * @type BaseView + */, + parent: null + /** + * Array of [view, selector]-pairs. + * @type Array<[BaseView, String]> + */, + subviews: [] + /** + * Whether this view has been added to the DOM. + * @type Boolean + */, + isAttached: false, + constructor: (function(){ + function BaseView(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + this.subviews = new ViewList; + this.onReturnKeypress = _.debounce(this.onReturnKeypress.bind(this), 50); + Backbone.View.apply(this, arguments); + return this.trigger('create', this); + } + return BaseView; + }()), + initialize: function(){ + this.__apply_bind__(); + this.setModel(this.model); + this.build(); + return this.$el.on('form submit', function(it){ + return it.preventDefault(); + }); + }, + setModel: function(model){ + var data; + if (this.model) { + this.model.off('change', this.render, this); + this.model.off('destroy', this.remove, this); + delete this.model.view; + data = this.$el.data(); + delete data.model; + delete data.view; + } + if (this.model = model) { + this.model.view = this; + this.$el.data({ + model: this.model, + view: this + }); + this.model.on('change', this.render, this); + this.model.on('destroy', this.remove, this); + this.trigger('change:model', this, model); + } + return model; + }, + setParent: function(parent){ + var old_parent, _ref; + _ref = [this.parent, parent], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('parent', this, parent, old_parent); + return this; + }, + unsetParent: function(){ + var old_parent, _ref; + _ref = [this.parent, null], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('unparent', this, old_parent); + return this; + }, + addSubview: function(view){ + this.removeSubview(view); + this.subviews.push(view); + view.setParent(this); + return view; + }, + removeSubview: function(view){ + if (this.hasSubview(view)) { + view.remove(); + this.subviews.remove(view); + view.unsetParent(); + } + return view; + }, + hasSubview: function(view){ + return this.subviews.contains(view); + }, + invokeSubviews: function(){ + var _ref; + return (_ref = this.subviews).invoke.apply(_ref, arguments); + }, + removeAllSubviews: function(){ + this.subviews.forEach(this.removeSubview, this); + return this; + }, + attach: function(el){ + var _this = this; + this.$el.appendTo(el); + if (this.isAttached) { + return this; + } + this.isAttached = true; + _.delay(function(){ + _this.delegateEvents(); + return _this.trigger('attach', _this); + }, 50); + return this; + }, + remove: function(){ + this.$el.remove(); + if (!this.isAttached) { + return this; + } + this.isAttached = false; + this.trigger('unattach', this); + return this; + }, + clear: function(){ + this.remove(); + this.model.destroy(); + this.trigger('clear', this); + return this; + }, + hide: function(){ + this.$el.hide(); + this.trigger('hide', this); + return this; + }, + show: function(){ + this.$el.show(); + this.trigger('show', this); + return this; + } + /** + * Attach each subview to its bind-point. + * @returns {this} + */, + attachSubviews: function(){ + var bps, view, bp, _i, _ref, _len; + bps = this.getOwnSubviewBindPoints(); + if (this.subviews.length && !bps.length) { + console.warn(this + ".attachSubviews(): no subview bind-points found!"); + return this; + } + for (_i = 0, _len = (_ref = this.subviews).length; _i < _len; ++_i) { + view = _ref[_i]; + if (bp = this.findSubviewBindPoint(view, bps)) { + view.attach(bp); + } else { + console.warn(this + ".attachSubviews(): Unable to find bind-point for " + view + "!"); + } + } + return this; + } + /** + * Finds all subview bind-points under this view's element, but not under + * the view element of any subview. + * @returns {jQuery|undefined} + */, + getOwnSubviewBindPoints: function(){ + return this.$('[data-subview]').not(this.$('[data-subview] [data-subview]')); + } + /** + * Find the matching subview bind-point for the given view. + */, + findSubviewBindPoint: function(view, bind_points){ + var bp; + bind_points || (bind_points = this.getOwnSubviewBindPoints()); + if (view.id) { + bp = bind_points.filter("[data-subview$=':" + view.id + "']"); + if (bp.length) { + return bp.eq(0); + } + } + bp = bind_points.filter("[data-subview='" + view.getClassName() + "']"); + if (bp.length) { + return bp.eq(0); + } + }, + toTemplateLocals: function(){ + return this.model.toJSON(); + }, + $template: function(){ + return $(this.template((__import({ + _: _, + op: op, + model: this.model, + view: this + }, this.toTemplateLocals())))); + }, + build: function(){ + var outer; + if (!this.template) { + return this; + } + outer = this.$template(); + this.$el.html(outer.html()).attr({ + id: outer.attr('id'), + 'class': outer.attr('class') + }); + this.attachSubviews(); + this.isBuilt = true; + return this; + }, + render: function(){ + this.wait(); + if (this.isBuilt) { + this.update(); + } else { + this.build(); + } + this.renderSubviews(); + this.trigger('render', this); + this.unwait(); + return this; + }, + renderSubviews: function(){ + this.attachSubviews(); + this.subviews.invoke('render'); + return this; + }, + update: function(){ + var locals; + new DataBinding(this).update(locals = this.toTemplateLocals()); + this.trigger('update', this, locals); + return this; + } + /* * * * Events * * * */, + bubbleEventDown: function(evt){ + this.invokeSubviews.apply(this, ['trigger'].concat(__slice.call(arguments))); + return this; + }, + redispatch: function(evt){ + var args; + args = __slice.call(arguments, 1); + this.trigger.apply(this, [evt, this].concat(__slice.call(args))); + return this; + }, + onlyOnReturn: function(fn){ + var args, _this = this; + args = __slice.call(arguments, 1); + fn = _.debounce(fn.bind(this), 50); + return function(evt){ + if (evt.keyCode === 13) { + return fn.apply(_this, args); + } + }; + } + /** + * Call a delegate on keypress == the return key. + * @returns {Function} Keypress event handler. + */, + onReturnKeypress: function(evt){ + var fn; + if (this.callOnReturnKeypress) { + fn = this[this.callOnReturnKeypress]; + } + if (fn && evt.keyCode === 13) { + return fn.call(this); + } + }, + toString: function(){ + return this.getClassName() + "(model=" + this.model + ")"; + } +})); +['get', 'set', 'unset', 'toJSON', 'toKV', 'toURL'].forEach(function(methodname){ + return BaseView.prototype[methodname] = function(){ + return this.model[methodname].apply(this.model, arguments); + }; +}); +exports.ViewList = ViewList = (function(superclass){ + ViewList.displayName = 'ViewList'; + var prototype = __extend(ViewList, superclass).prototype, constructor = ViewList; + function ViewList(views){ + views == null && (views = []); + superclass.apply(this, arguments); + } + prototype.extend = function(views){ + var _this = this; + _.each(views, function(it){ + return _this.push(it); + }); + return this; + }; + prototype.findByModel = function(model){ + return this.find(function(it){ + return it.model === model; + }); + }; + prototype.toString = function(){ + var contents; + contents = this.length ? "\"" + this.join('","') + "\"" : ''; + return "ViewList[" + this.length + "](" + contents + ")"; + }; + return ViewList; +}(Array)); +['each', 'contains', 'invoke', 'pluck', 'find', 'remove', 'compact', 'flatten', 'without', 'union', 'intersection', 'difference', 'unique', 'uniq'].forEach(function(methodname){ + return ViewList.prototype[methodname] = function(){ + var _ref; + return (_ref = _[methodname]).call.apply(_ref, [_, this].concat(__slice.call(arguments))); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/base/base-view.mod.js b/lib/base/base-view.mod.js new file mode 100644 index 0000000..2105eea --- /dev/null +++ b/lib/base/base-view.mod.js @@ -0,0 +1,334 @@ +require.define('/node_modules/kraken/base/base-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, DataBinding, BaseView, ViewList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +BaseModel = require('kraken/base/base-mixin').BaseModel; +DataBinding = require('kraken/base/data-binding').DataBinding; +/** + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View + */ +BaseView = exports.BaseView = Backbone.View.extend(mixinBase({ + tagName: 'section', + model: BaseModel + /** + * Method-name called by `onReturnKeypress` when used as an event-handler. + * @type String + */, + callOnReturnKeypress: null + /** + * Parent view of this view. + * @type BaseView + */, + parent: null + /** + * Array of [view, selector]-pairs. + * @type Array<[BaseView, String]> + */, + subviews: [] + /** + * Whether this view has been added to the DOM. + * @type Boolean + */, + isAttached: false, + constructor: (function(){ + function BaseView(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + this.subviews = new ViewList; + this.onReturnKeypress = _.debounce(this.onReturnKeypress.bind(this), 50); + Backbone.View.apply(this, arguments); + return this.trigger('create', this); + } + return BaseView; + }()), + initialize: function(){ + this.__apply_bind__(); + this.setModel(this.model); + this.build(); + return this.$el.on('form submit', function(it){ + return it.preventDefault(); + }); + }, + setModel: function(model){ + var data; + if (this.model) { + this.model.off('change', this.render, this); + this.model.off('destroy', this.remove, this); + delete this.model.view; + data = this.$el.data(); + delete data.model; + delete data.view; + } + if (this.model = model) { + this.model.view = this; + this.$el.data({ + model: this.model, + view: this + }); + this.model.on('change', this.render, this); + this.model.on('destroy', this.remove, this); + this.trigger('change:model', this, model); + } + return model; + }, + setParent: function(parent){ + var old_parent, _ref; + _ref = [this.parent, parent], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('parent', this, parent, old_parent); + return this; + }, + unsetParent: function(){ + var old_parent, _ref; + _ref = [this.parent, null], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('unparent', this, old_parent); + return this; + }, + addSubview: function(view){ + this.removeSubview(view); + this.subviews.push(view); + view.setParent(this); + return view; + }, + removeSubview: function(view){ + if (this.hasSubview(view)) { + view.remove(); + this.subviews.remove(view); + view.unsetParent(); + } + return view; + }, + hasSubview: function(view){ + return this.subviews.contains(view); + }, + invokeSubviews: function(){ + var _ref; + return (_ref = this.subviews).invoke.apply(_ref, arguments); + }, + removeAllSubviews: function(){ + this.subviews.forEach(this.removeSubview, this); + return this; + }, + attach: function(el){ + var _this = this; + this.$el.appendTo(el); + if (this.isAttached) { + return this; + } + this.isAttached = true; + _.delay(function(){ + _this.delegateEvents(); + return _this.trigger('attach', _this); + }, 50); + return this; + }, + remove: function(){ + this.$el.remove(); + if (!this.isAttached) { + return this; + } + this.isAttached = false; + this.trigger('unattach', this); + return this; + }, + clear: function(){ + this.remove(); + this.model.destroy(); + this.trigger('clear', this); + return this; + }, + hide: function(){ + this.$el.hide(); + this.trigger('hide', this); + return this; + }, + show: function(){ + this.$el.show(); + this.trigger('show', this); + return this; + } + /** + * Attach each subview to its bind-point. + * @returns {this} + */, + attachSubviews: function(){ + var bps, view, bp, _i, _ref, _len; + bps = this.getOwnSubviewBindPoints(); + if (this.subviews.length && !bps.length) { + console.warn(this + ".attachSubviews(): no subview bind-points found!"); + return this; + } + for (_i = 0, _len = (_ref = this.subviews).length; _i < _len; ++_i) { + view = _ref[_i]; + if (bp = this.findSubviewBindPoint(view, bps)) { + view.attach(bp); + } else { + console.warn(this + ".attachSubviews(): Unable to find bind-point for " + view + "!"); + } + } + return this; + } + /** + * Finds all subview bind-points under this view's element, but not under + * the view element of any subview. + * @returns {jQuery|undefined} + */, + getOwnSubviewBindPoints: function(){ + return this.$('[data-subview]').not(this.$('[data-subview] [data-subview]')); + } + /** + * Find the matching subview bind-point for the given view. + */, + findSubviewBindPoint: function(view, bind_points){ + var bp; + bind_points || (bind_points = this.getOwnSubviewBindPoints()); + if (view.id) { + bp = bind_points.filter("[data-subview$=':" + view.id + "']"); + if (bp.length) { + return bp.eq(0); + } + } + bp = bind_points.filter("[data-subview='" + view.getClassName() + "']"); + if (bp.length) { + return bp.eq(0); + } + }, + toTemplateLocals: function(){ + return this.model.toJSON(); + }, + $template: function(){ + return $(this.template((__import({ + _: _, + op: op, + model: this.model, + view: this + }, this.toTemplateLocals())))); + }, + build: function(){ + var outer; + if (!this.template) { + return this; + } + outer = this.$template(); + this.$el.html(outer.html()).attr({ + id: outer.attr('id'), + 'class': outer.attr('class') + }); + this.attachSubviews(); + this.isBuilt = true; + return this; + }, + render: function(){ + this.wait(); + if (this.isBuilt) { + this.update(); + } else { + this.build(); + } + this.renderSubviews(); + this.trigger('render', this); + this.unwait(); + return this; + }, + renderSubviews: function(){ + this.attachSubviews(); + this.subviews.invoke('render'); + return this; + }, + update: function(){ + var locals; + new DataBinding(this).update(locals = this.toTemplateLocals()); + this.trigger('update', this, locals); + return this; + } + /* * * * Events * * * */, + bubbleEventDown: function(evt){ + this.invokeSubviews.apply(this, ['trigger'].concat(__slice.call(arguments))); + return this; + }, + redispatch: function(evt){ + var args; + args = __slice.call(arguments, 1); + this.trigger.apply(this, [evt, this].concat(__slice.call(args))); + return this; + }, + onlyOnReturn: function(fn){ + var args, _this = this; + args = __slice.call(arguments, 1); + fn = _.debounce(fn.bind(this), 50); + return function(evt){ + if (evt.keyCode === 13) { + return fn.apply(_this, args); + } + }; + } + /** + * Call a delegate on keypress == the return key. + * @returns {Function} Keypress event handler. + */, + onReturnKeypress: function(evt){ + var fn; + if (this.callOnReturnKeypress) { + fn = this[this.callOnReturnKeypress]; + } + if (fn && evt.keyCode === 13) { + return fn.call(this); + } + }, + toString: function(){ + return this.getClassName() + "(model=" + this.model + ")"; + } +})); +['get', 'set', 'unset', 'toJSON', 'toKV', 'toURL'].forEach(function(methodname){ + return BaseView.prototype[methodname] = function(){ + return this.model[methodname].apply(this.model, arguments); + }; +}); +exports.ViewList = ViewList = (function(superclass){ + ViewList.displayName = 'ViewList'; + var prototype = __extend(ViewList, superclass).prototype, constructor = ViewList; + function ViewList(views){ + views == null && (views = []); + superclass.apply(this, arguments); + } + prototype.extend = function(views){ + var _this = this; + _.each(views, function(it){ + return _this.push(it); + }); + return this; + }; + prototype.findByModel = function(model){ + return this.find(function(it){ + return it.model === model; + }); + }; + prototype.toString = function(){ + var contents; + contents = this.length ? "\"" + this.join('","') + "\"" : ''; + return "ViewList[" + this.length + "](" + contents + ")"; + }; + return ViewList; +}(Array)); +['each', 'contains', 'invoke', 'pluck', 'find', 'remove', 'compact', 'flatten', 'without', 'union', 'intersection', 'difference', 'unique', 'uniq'].forEach(function(methodname){ + return ViewList.prototype[methodname] = function(){ + var _ref; + return (_ref = _[methodname]).call.apply(_ref, [_, this].concat(__slice.call(arguments))); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/base/base.js b/lib/base/base.js new file mode 100644 index 0000000..e3f4bce --- /dev/null +++ b/lib/base/base.js @@ -0,0 +1,77 @@ +var EventEmitter, op, Base, k, _ref, _, _i, _len, __slice = [].slice; +EventEmitter = require('events').EventEmitter; +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; +EventEmitter.prototype.trigger = EventEmitter.prototype.emit; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Eventful base class. + * @extends EventEmitter + */ +Base = (function(superclass){ + /** + * After the super chain has exhausted (but not necessarily at the end + * of init -- it depends on when you super()), Base will publish a 'new' + * event on the instance's class, allowing anyone to subscribe to + * notifications about new objects. + * @constructor + */ + Base.displayName = 'Base'; + var prototype = __extend(Base, superclass).prototype, constructor = Base; + function Base(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.superclass; + this.__apply_bind__(); + superclass.call(this); + this.__class__.emit('new', this); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */ + prototype.__bind__ = []; + /** + * Applies the contents of `__bind__`. + */ + prototype.__apply_bind__ = function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + }; + prototype.getClassName = function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }; + prototype.toString = function(){ + return this.getClassName() + "()"; + }; + Base.extended = function(Subclass){ + var k, v, _own = {}.hasOwnProperty; + for (k in this) if (_own.call(this, k)) { + v = this[k]; + if (typeof v === 'function') { + Subclass[k] = v; + } + } + Subclass.__super__ = this.prototype; + return Subclass; + }; + return Base; +}(EventEmitter)); +for (_i = 0, _len = (_ref = ['getSuperClasses', 'pluckSuper', 'pluckSuperAndSelf']).length; _i < _len; ++_i) { + k = _ref[_i]; + Base[k] = Base.prototype[k] = _.methodize(_[k]); +} +__import(Base, EventEmitter.prototype); +module.exports = Base; +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base.mod.js b/lib/base/base.mod.js new file mode 100644 index 0000000..68ac0c5 --- /dev/null +++ b/lib/base/base.mod.js @@ -0,0 +1,81 @@ +require.define('/node_modules/kraken/base/base.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var EventEmitter, op, Base, k, _ref, _, _i, _len, __slice = [].slice; +EventEmitter = require('events').EventEmitter; +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; +EventEmitter.prototype.trigger = EventEmitter.prototype.emit; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Eventful base class. + * @extends EventEmitter + */ +Base = (function(superclass){ + /** + * After the super chain has exhausted (but not necessarily at the end + * of init -- it depends on when you super()), Base will publish a 'new' + * event on the instance's class, allowing anyone to subscribe to + * notifications about new objects. + * @constructor + */ + Base.displayName = 'Base'; + var prototype = __extend(Base, superclass).prototype, constructor = Base; + function Base(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.superclass; + this.__apply_bind__(); + superclass.call(this); + this.__class__.emit('new', this); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */ + prototype.__bind__ = []; + /** + * Applies the contents of `__bind__`. + */ + prototype.__apply_bind__ = function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + }; + prototype.getClassName = function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }; + prototype.toString = function(){ + return this.getClassName() + "()"; + }; + Base.extended = function(Subclass){ + var k, v, _own = {}.hasOwnProperty; + for (k in this) if (_own.call(this, k)) { + v = this[k]; + if (typeof v === 'function') { + Subclass[k] = v; + } + } + Subclass.__super__ = this.prototype; + return Subclass; + }; + return Base; +}(EventEmitter)); +for (_i = 0, _len = (_ref = ['getSuperClasses', 'pluckSuper', 'pluckSuperAndSelf']).length; _i < _len; ++_i) { + k = _ref[_i]; + Base[k] = Base.prototype[k] = _.methodize(_[k]); +} +__import(Base, EventEmitter.prototype); +module.exports = Base; +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/cascading-model.js b/lib/base/cascading-model.js new file mode 100644 index 0000000..8a1f89c --- /dev/null +++ b/lib/base/cascading-model.js @@ -0,0 +1,54 @@ +var op, BaseModel, BaseList, Cascade, CascadingModel, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-model'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Cascade = require('kraken/util/cascade'); +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend({ + /** + * The lookup cascade. + * @type Cascade + */ + cascade: null, + constructor: (function(){ + function CascadingModel(attributes, opts){ + attributes == null && (attributes = {}); + this.cascade = new Cascade(attributes); + return BaseModel.call(this, attributes, opts); + } + return CascadingModel; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + } + /** + * Recursively look up a (potenitally nested) attribute in the lookup chain. + * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys). + * @returns {*} + */, + get: function(key){ + return this.cascade.get(key); + }, + toJSON: function(opts){ + opts == null && (opts = {}); + opts = (__import({ + collapseCascade: false + }, opts)); + if (opts.collapseCascade) { + return this.cascade.collapse(); + } else { + return BaseModel.prototype.toJSON.apply(this, arguments); + } + } +}); +['addLookup', 'removeLookup', 'popLookup', 'shiftLookup', 'unshiftLookup', 'isOwnProperty', 'isOwnValue', 'isInheritedValue', 'isModifiedValue'].forEach(function(methodname){ + return CascadingModel.prototype[methodname] = function(){ + return this.cascade[methodname].apply(this.cascade, arguments); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/cascading-model.mod.js b/lib/base/cascading-model.mod.js new file mode 100644 index 0000000..5fc62d5 --- /dev/null +++ b/lib/base/cascading-model.mod.js @@ -0,0 +1,58 @@ +require.define('/node_modules/kraken/base/cascading-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseModel, BaseList, Cascade, CascadingModel, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-model'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Cascade = require('kraken/util/cascade'); +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend({ + /** + * The lookup cascade. + * @type Cascade + */ + cascade: null, + constructor: (function(){ + function CascadingModel(attributes, opts){ + attributes == null && (attributes = {}); + this.cascade = new Cascade(attributes); + return BaseModel.call(this, attributes, opts); + } + return CascadingModel; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + } + /** + * Recursively look up a (potenitally nested) attribute in the lookup chain. + * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys). + * @returns {*} + */, + get: function(key){ + return this.cascade.get(key); + }, + toJSON: function(opts){ + opts == null && (opts = {}); + opts = (__import({ + collapseCascade: false + }, opts)); + if (opts.collapseCascade) { + return this.cascade.collapse(); + } else { + return BaseModel.prototype.toJSON.apply(this, arguments); + } + } +}); +['addLookup', 'removeLookup', 'popLookup', 'shiftLookup', 'unshiftLookup', 'isOwnProperty', 'isOwnValue', 'isInheritedValue', 'isModifiedValue'].forEach(function(methodname){ + return CascadingModel.prototype[methodname] = function(){ + return this.cascade[methodname].apply(this.cascade, arguments); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/data-binding.js b/lib/base/data-binding.js new file mode 100644 index 0000000..9f1835c --- /dev/null +++ b/lib/base/data-binding.js @@ -0,0 +1,65 @@ +var Backbone, op, DataBinding, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +exports.DataBinding = DataBinding = (function(){ + DataBinding.displayName = 'DataBinding'; + var prototype = DataBinding.prototype, constructor = DataBinding; + prototype.data = null; + prototype.context = null; + prototype.el = null; + prototype.$el = null; + prototype.bindPoints = null; + function DataBinding(el, context){ + this.context = context != null ? context : el; + if (el instanceof Backbone.View) { + el = el.$el; + } + this.$el = $(el); + this.el = this.$el.get(0); + this.bindPoints = this.$('[data-bind], [name]').not(this.$('[data-subview]').find('[data-bind], [name]')); + } + prototype.$ = function(sel){ + return this.$el.find(sel); + }; + prototype.serialize = function(it){ + return it; + }; + prototype.update = function(data){ + var key, val, _ref; + this.data = data; + for (key in _ref = _.collapseObject(this.data)) { + val = _ref[key]; + this.updateBinding(key, val); + } + return this; + }; + prototype.updateBinding = function(key, val){ + var bp; + if (bp = this.findDataBindPoint(key)) { + if (_.isFunction(val)) { + val.call(this.context, val, key, bp, this.data); + } else if (bp.is('input:checkbox')) { + bp.attr('checked', !!val); + } else if (bp.is('input, textarea')) { + bp.val(this.serialize(val)); + } else { + if (op.toBool(bp.data('data-bind-escape'))) { + bp.text(this.serialize(val)); + } else { + bp.html(this.serialize(val)); + } + } + } else { + false && console.warn(this + ".updateBinding(): Unable to find data bind-point for " + key + "=" + val + "!"); + } + return this; + }; + prototype.findDataBindPoint = function(key){ + var bp; + bp = this.bindPoints.filter("[name='" + key + "'], [data-bind='" + key + "']"); + if (bp.length) { + return bp.eq(0); + } + }; + return DataBinding; +}()); \ No newline at end of file diff --git a/lib/base/data-binding.mod.js b/lib/base/data-binding.mod.js new file mode 100644 index 0000000..957bc2c --- /dev/null +++ b/lib/base/data-binding.mod.js @@ -0,0 +1,69 @@ +require.define('/node_modules/kraken/base/data-binding.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, DataBinding, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +exports.DataBinding = DataBinding = (function(){ + DataBinding.displayName = 'DataBinding'; + var prototype = DataBinding.prototype, constructor = DataBinding; + prototype.data = null; + prototype.context = null; + prototype.el = null; + prototype.$el = null; + prototype.bindPoints = null; + function DataBinding(el, context){ + this.context = context != null ? context : el; + if (el instanceof Backbone.View) { + el = el.$el; + } + this.$el = $(el); + this.el = this.$el.get(0); + this.bindPoints = this.$('[data-bind], [name]').not(this.$('[data-subview]').find('[data-bind], [name]')); + } + prototype.$ = function(sel){ + return this.$el.find(sel); + }; + prototype.serialize = function(it){ + return it; + }; + prototype.update = function(data){ + var key, val, _ref; + this.data = data; + for (key in _ref = _.collapseObject(this.data)) { + val = _ref[key]; + this.updateBinding(key, val); + } + return this; + }; + prototype.updateBinding = function(key, val){ + var bp; + if (bp = this.findDataBindPoint(key)) { + if (_.isFunction(val)) { + val.call(this.context, val, key, bp, this.data); + } else if (bp.is('input:checkbox')) { + bp.attr('checked', !!val); + } else if (bp.is('input, textarea')) { + bp.val(this.serialize(val)); + } else { + if (op.toBool(bp.data('data-bind-escape'))) { + bp.text(this.serialize(val)); + } else { + bp.html(this.serialize(val)); + } + } + } else { + false && console.warn(this + ".updateBinding(): Unable to find data bind-point for " + key + "=" + val + "!"); + } + return this; + }; + prototype.findDataBindPoint = function(key){ + var bp; + bp = this.bindPoints.filter("[name='" + key + "'], [data-bind='" + key + "']"); + if (bp.length) { + return bp.eq(0); + } + }; + return DataBinding; +}()); + +}); diff --git a/lib/base/index.js b/lib/base/index.js new file mode 100644 index 0000000..1c9458c --- /dev/null +++ b/lib/base/index.js @@ -0,0 +1,14 @@ +var mixins, models, views, cache, cascading, data_binding; +exports.Base = require('kraken/base/base'); +mixins = require('kraken/base/base-mixin'); +models = require('kraken/base/base-model'); +views = require('kraken/base/base-view'); +cache = require('kraken/base/model-cache'); +cascading = require('kraken/base/cascading-model'); +data_binding = require('kraken/base/data-binding'); +__import(__import(__import(__import(__import(__import(exports, mixins), models), views), cache), cascading), data_binding); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/index.mod.js b/lib/base/index.mod.js new file mode 100644 index 0000000..a599753 --- /dev/null +++ b/lib/base/index.mod.js @@ -0,0 +1,18 @@ +require.define('/node_modules/kraken/base/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var mixins, models, views, cache, cascading, data_binding; +exports.Base = require('kraken/base/base'); +mixins = require('kraken/base/base-mixin'); +models = require('kraken/base/base-model'); +views = require('kraken/base/base-view'); +cache = require('kraken/base/model-cache'); +cascading = require('kraken/base/cascading-model'); +data_binding = require('kraken/base/data-binding'); +__import(__import(__import(__import(__import(__import(exports, mixins), models), views), cache), cascading), data_binding); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/model-cache.js b/lib/base/model-cache.js new file mode 100644 index 0000000..f07c776 --- /dev/null +++ b/lib/base/model-cache.js @@ -0,0 +1,218 @@ +var Seq, ReadyEmitter, ModelCache, _; +_ = require('underscore'); +Seq = require('seq'); +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +/** + * @class Caches models and provides static lookups by ID. + */ +exports.ModelCache = ModelCache = (function(superclass){ + ModelCache.displayName = 'ModelCache'; + var prototype = __extend(ModelCache, superclass).prototype, constructor = ModelCache; + /** + * @see ReadyEmitter#readyEventName + * @private + * @constant + * @type String + */ + prototype.readyEventName = 'cache-ready'; + /** + * Default options. + * @private + * @constant + * @type Object + */ + prototype.DEFAULT_OPTIONS = { + ready: true, + cache: null, + create: null, + ModelType: null + }; + /** + * @private + * @type Object + */ + prototype.options = null; + /** + * Type we're caching (presumably extending `Backbone.Model`), used to create new + * instances unless a `create` function was provided in options. + * @private + * @type Class + */ + prototype.ModelType = null; + /** + * Collection holding the cached Models. + * @private + * @type Backbone.Collection + */ + prototype.cache = null; + /** + * @constructor + * @param {Class} [ModelType] Type of cached object (presumably extending + * `Backbone.Model`), used to create new instances unless `options.create` + * is provided. + * @param {Object} [options] Options: + * @param {Boolean} [options.ready=true] Starting `ready` state. If false, + * the cache will queue lookup calls until `triggerReady()` is called. + * @param {Class} [options.cache=new Backbone.Collection] + * The backing data-structure for the cache. If omitted, we'll use a new + * `Backbone.Collection`, but really, anything with a `get(id)` method for + * model lookup will work here. + * @param {Function} [options.create] A function called when a new Model + * object is needed, being passed the new model ID. + * @param {Class} [options.ModelType] Type of cached object + * (presumably extending `Backbone.Model`), used to create new instances + * unless `options.create` is provided. + */; + function ModelCache(ModelType, options){ + var that, _ref; + if (!_.isFunction(ModelType)) { + _ref = [ModelType || {}, null], options = _ref[0], ModelType = _ref[1]; + } + this.options = (_ref = {}, __import(_ref, this.DEFAULT_OPTIONS), __import(_ref, options)); + this.cache = this.options.cache || new Backbone.Collection; + this.ModelType = ModelType || this.options.ModelType; + if (that = this.options.create) { + this.createModel = that; + } + this.ready = !!this.options.ready; + if (this.ModelType) { + this.decorate(this.ModelType); + } + } + /** + * Called when a new Model object is needed, being passed the new model ID. + * Uses the supplied `ModelType`; overriden by `options.create` if provided. + * + * @param {String} id The model ID to create. + * @returns {Model} Created model. + */ + prototype.createModel = function(id){ + return new this.ModelType({ + id: id + }); + }; + /** + * Registers a model with the cache. If a model by this ID already exists + * in the cache, it will be removed and this one will take its place. + * + * Fires an `add` event. + * + * @param {Model} model The model. + * @returns {Model} The model. + */ + prototype.register = function(model){ + if (this.cache.contains(model)) { + this.cache.remove(model, { + silent: true + }); + } + this.cache.add(model); + this.trigger('add', this, model); + return model; + }; + /** + * Synchronously check if a model is in the cache, returning it if so. + * + * @param {String} id The model ID to get. + * @returns {Model} + */ + prototype.get = function(id){ + return this.cache.get(id); + }; + /** + * Asynchronously look up any number of models, requesting them from the + * server if not already known to the cache. + * + * @param {String|Array} ids List of model IDs to lookup. + * @param {Function} cb Callback of the form `(err, models)`, + * where `err` will be null on success and `models` will be an Array + * of model objects. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + prototype.lookupAll = function(ids, cb, cxt){ + var _this = this; + cxt == null && (cxt = this); + if (!_.isArray(ids)) { + ids = [ids]; + } + if (!this.ready) { + this.on('cache-ready', function(){ + _this.off('cache-ready', arguments.callee); + return _this.lookupAll(ids, cb, cxt); + }); + return this; + } + Seq(ids).parMap_(function(next, id){ + var that; + if (that = _this.cache.get(id)) { + return next.ok(that); + } + return _this.register(_this.createModel(id)).on('ready', function(it){ + return next.ok(it); + }).load(); + }).unflatten().seq(function(models){ + return cb.call(cxt, null, models); + })['catch'](function(err){ + return cb.call(cxt, err); + }); + return this; + }; + /** + * Looks up a model, requesting it from the server if it is not already + * known to the cache. + * + * @param {String|Array} id Model ID to lookup. + * @param {Function} cb Callback of the form `(err, model)`, + * where `err` will be null on success and `model` will be the + * model object. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + prototype.lookup = function(id, cb, cxt){ + cxt == null && (cxt = this); + return this.lookupAll([id], function(err, models){ + if (err) { + return cb.call(cxt, err); + } else { + return cb.call(cxt, null, models[0]); + } + }); + }; + /** + * Decorate an object with the cache methods: + * - register + * - get + * - lookup + * - lookupAll + * + * This is automatically called on `ModelType` if supplied. + * + * @param {Object} obj Object to decorate. + * @returns {obj} The supplied object. + */ + prototype.decorate = function(obj){ + var m, _i, _ref, _len; + obj.__cache__ = this; + for (_i = 0, _len = (_ref = ['register', 'get', 'lookup', 'lookupAll']).length; _i < _len; ++_i) { + m = _ref[_i]; + obj[m] = this[m].bind(this); + } + return obj; + }; + prototype.toString = function(){ + return (this.constructor.displayName || this.constructor.name) + "(cache=" + this.cache + ")"; + }; + return ModelCache; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/model-cache.mod.js b/lib/base/model-cache.mod.js new file mode 100644 index 0000000..4e529f9 --- /dev/null +++ b/lib/base/model-cache.mod.js @@ -0,0 +1,222 @@ +require.define('/node_modules/kraken/base/model-cache.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, ReadyEmitter, ModelCache, _; +_ = require('underscore'); +Seq = require('seq'); +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +/** + * @class Caches models and provides static lookups by ID. + */ +exports.ModelCache = ModelCache = (function(superclass){ + ModelCache.displayName = 'ModelCache'; + var prototype = __extend(ModelCache, superclass).prototype, constructor = ModelCache; + /** + * @see ReadyEmitter#readyEventName + * @private + * @constant + * @type String + */ + prototype.readyEventName = 'cache-ready'; + /** + * Default options. + * @private + * @constant + * @type Object + */ + prototype.DEFAULT_OPTIONS = { + ready: true, + cache: null, + create: null, + ModelType: null + }; + /** + * @private + * @type Object + */ + prototype.options = null; + /** + * Type we're caching (presumably extending `Backbone.Model`), used to create new + * instances unless a `create` function was provided in options. + * @private + * @type Class + */ + prototype.ModelType = null; + /** + * Collection holding the cached Models. + * @private + * @type Backbone.Collection + */ + prototype.cache = null; + /** + * @constructor + * @param {Class} [ModelType] Type of cached object (presumably extending + * `Backbone.Model`), used to create new instances unless `options.create` + * is provided. + * @param {Object} [options] Options: + * @param {Boolean} [options.ready=true] Starting `ready` state. If false, + * the cache will queue lookup calls until `triggerReady()` is called. + * @param {Class} [options.cache=new Backbone.Collection] + * The backing data-structure for the cache. If omitted, we'll use a new + * `Backbone.Collection`, but really, anything with a `get(id)` method for + * model lookup will work here. + * @param {Function} [options.create] A function called when a new Model + * object is needed, being passed the new model ID. + * @param {Class} [options.ModelType] Type of cached object + * (presumably extending `Backbone.Model`), used to create new instances + * unless `options.create` is provided. + */; + function ModelCache(ModelType, options){ + var that, _ref; + if (!_.isFunction(ModelType)) { + _ref = [ModelType || {}, null], options = _ref[0], ModelType = _ref[1]; + } + this.options = (_ref = {}, __import(_ref, this.DEFAULT_OPTIONS), __import(_ref, options)); + this.cache = this.options.cache || new Backbone.Collection; + this.ModelType = ModelType || this.options.ModelType; + if (that = this.options.create) { + this.createModel = that; + } + this.ready = !!this.options.ready; + if (this.ModelType) { + this.decorate(this.ModelType); + } + } + /** + * Called when a new Model object is needed, being passed the new model ID. + * Uses the supplied `ModelType`; overriden by `options.create` if provided. + * + * @param {String} id The model ID to create. + * @returns {Model} Created model. + */ + prototype.createModel = function(id){ + return new this.ModelType({ + id: id + }); + }; + /** + * Registers a model with the cache. If a model by this ID already exists + * in the cache, it will be removed and this one will take its place. + * + * Fires an `add` event. + * + * @param {Model} model The model. + * @returns {Model} The model. + */ + prototype.register = function(model){ + if (this.cache.contains(model)) { + this.cache.remove(model, { + silent: true + }); + } + this.cache.add(model); + this.trigger('add', this, model); + return model; + }; + /** + * Synchronously check if a model is in the cache, returning it if so. + * + * @param {String} id The model ID to get. + * @returns {Model} + */ + prototype.get = function(id){ + return this.cache.get(id); + }; + /** + * Asynchronously look up any number of models, requesting them from the + * server if not already known to the cache. + * + * @param {String|Array} ids List of model IDs to lookup. + * @param {Function} cb Callback of the form `(err, models)`, + * where `err` will be null on success and `models` will be an Array + * of model objects. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + prototype.lookupAll = function(ids, cb, cxt){ + var _this = this; + cxt == null && (cxt = this); + if (!_.isArray(ids)) { + ids = [ids]; + } + if (!this.ready) { + this.on('cache-ready', function(){ + _this.off('cache-ready', arguments.callee); + return _this.lookupAll(ids, cb, cxt); + }); + return this; + } + Seq(ids).parMap_(function(next, id){ + var that; + if (that = _this.cache.get(id)) { + return next.ok(that); + } + return _this.register(_this.createModel(id)).on('ready', function(it){ + return next.ok(it); + }).load(); + }).unflatten().seq(function(models){ + return cb.call(cxt, null, models); + })['catch'](function(err){ + return cb.call(cxt, err); + }); + return this; + }; + /** + * Looks up a model, requesting it from the server if it is not already + * known to the cache. + * + * @param {String|Array} id Model ID to lookup. + * @param {Function} cb Callback of the form `(err, model)`, + * where `err` will be null on success and `model` will be the + * model object. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + prototype.lookup = function(id, cb, cxt){ + cxt == null && (cxt = this); + return this.lookupAll([id], function(err, models){ + if (err) { + return cb.call(cxt, err); + } else { + return cb.call(cxt, null, models[0]); + } + }); + }; + /** + * Decorate an object with the cache methods: + * - register + * - get + * - lookup + * - lookupAll + * + * This is automatically called on `ModelType` if supplied. + * + * @param {Object} obj Object to decorate. + * @returns {obj} The supplied object. + */ + prototype.decorate = function(obj){ + var m, _i, _ref, _len; + obj.__cache__ = this; + for (_i = 0, _len = (_ref = ['register', 'get', 'lookup', 'lookupAll']).length; _i < _len; ++_i) { + m = _ref[_i]; + obj[m] = this[m].bind(this); + } + return obj; + }; + prototype.toString = function(){ + return (this.constructor.displayName || this.constructor.name) + "(cache=" + this.cache + ")"; + }; + return ModelCache; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/index.js b/lib/base/scaffold/index.js new file mode 100644 index 0000000..c0ee89d --- /dev/null +++ b/lib/base/scaffold/index.js @@ -0,0 +1,9 @@ +var models, views; +models = require('kraken/base/scaffold/scaffold-model'); +views = require('kraken/base/scaffold/scaffold-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/scaffold/index.mod.js b/lib/base/scaffold/index.mod.js new file mode 100644 index 0000000..2278e76 --- /dev/null +++ b/lib/base/scaffold/index.mod.js @@ -0,0 +1,13 @@ +require.define('/node_modules/kraken/base/scaffold/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var models, views; +models = require('kraken/base/scaffold/scaffold-model'); +views = require('kraken/base/scaffold/scaffold-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/scaffold-model.js b/lib/base/scaffold/scaffold-model.js new file mode 100644 index 0000000..948a703 --- /dev/null +++ b/lib/base/scaffold/scaffold-model.js @@ -0,0 +1,131 @@ +var op, BaseModel, BaseList, Field, FieldList, _, _ref, __slice = [].slice; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Field = exports.Field = BaseModel.extend({ + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function Field(){ + return BaseModel.apply(this, arguments); + } + return Field; + }()), + initialize: function(){ + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + return Field.__super__.initialize.apply(this, arguments); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serializers * * */, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +FieldList = exports.FieldList = BaseList.extend({ + model: Field, + constructor: (function(){ + function FieldList(){ + return BaseList.apply(this, arguments); + } + return FieldList; + }()) + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */, + values: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: true, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/scaffold/scaffold-model.mod.js b/lib/base/scaffold/scaffold-model.mod.js new file mode 100644 index 0000000..ee06f50 --- /dev/null +++ b/lib/base/scaffold/scaffold-model.mod.js @@ -0,0 +1,135 @@ +require.define('/node_modules/kraken/base/scaffold/scaffold-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseModel, BaseList, Field, FieldList, _, _ref, __slice = [].slice; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Field = exports.Field = BaseModel.extend({ + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function Field(){ + return BaseModel.apply(this, arguments); + } + return Field; + }()), + initialize: function(){ + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + return Field.__super__.initialize.apply(this, arguments); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serializers * * */, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +FieldList = exports.FieldList = BaseList.extend({ + model: Field, + constructor: (function(){ + function FieldList(){ + return BaseList.apply(this, arguments); + } + return FieldList; + }()) + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */, + values: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: true, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/scaffold-view.js b/lib/base/scaffold/scaffold-view.js new file mode 100644 index 0000000..2bc3791 --- /dev/null +++ b/lib/base/scaffold/scaffold-view.js @@ -0,0 +1,116 @@ +var op, BaseView, Field, FieldList, FieldView, Scaffold, _, _ref; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/base/scaffold/scaffold-model'), Field = _ref.Field, FieldList = _ref.FieldList; +FieldView = exports.FieldView = BaseView.extend({ + tagName: 'div', + className: 'field', + type: 'string', + events: { + 'blur .value': 'onChange', + 'submit .value': 'onChange' + }, + constructor: (function(){ + function FieldView(){ + return BaseView.apply(this, arguments); + } + return FieldView; + }()), + initialize: function(){ + BaseView.prototype.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + }, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this); + }, + toTemplateLocals: function(){ + var json, v; + json = FieldView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + if (v = json.value && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return json; + } + /** + * A ghetto default template, typically overridden by superclass. + */, + template: function(locals){ + return $("\n"); + }, + render: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + return FieldView.__super__.render.apply(this, arguments); + } +}); +Scaffold = exports.Scaffold = BaseView.extend({ + __bind__: ['addField', 'resetFields'], + tagName: 'form', + className: 'scaffold', + collectionType: FieldList, + subviewType: FieldView, + constructor: (function(){ + function Scaffold(){ + return BaseView.apply(this, arguments); + } + return Scaffold; + }()), + initialize: function(){ + var CollectionType; + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + BaseView.prototype.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + return this.collection.on('reset', this.resetFields, this); + }, + addField: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + view = this.addSubview(new SubviewType({ + model: field + })); + view.on('change', this.onChange.bind(this, field)); + this.render(); + return view; + }, + resetFields: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return this; + }, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return Scaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); \ No newline at end of file diff --git a/lib/base/scaffold/scaffold-view.mod.js b/lib/base/scaffold/scaffold-view.mod.js new file mode 100644 index 0000000..f71a8da --- /dev/null +++ b/lib/base/scaffold/scaffold-view.mod.js @@ -0,0 +1,120 @@ +require.define('/node_modules/kraken/base/scaffold/scaffold-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseView, Field, FieldList, FieldView, Scaffold, _, _ref; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/base/scaffold/scaffold-model'), Field = _ref.Field, FieldList = _ref.FieldList; +FieldView = exports.FieldView = BaseView.extend({ + tagName: 'div', + className: 'field', + type: 'string', + events: { + 'blur .value': 'onChange', + 'submit .value': 'onChange' + }, + constructor: (function(){ + function FieldView(){ + return BaseView.apply(this, arguments); + } + return FieldView; + }()), + initialize: function(){ + BaseView.prototype.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + }, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this); + }, + toTemplateLocals: function(){ + var json, v; + json = FieldView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + if (v = json.value && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return json; + } + /** + * A ghetto default template, typically overridden by superclass. + */, + template: function(locals){ + return $("\n"); + }, + render: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + return FieldView.__super__.render.apply(this, arguments); + } +}); +Scaffold = exports.Scaffold = BaseView.extend({ + __bind__: ['addField', 'resetFields'], + tagName: 'form', + className: 'scaffold', + collectionType: FieldList, + subviewType: FieldView, + constructor: (function(){ + function Scaffold(){ + return BaseView.apply(this, arguments); + } + return Scaffold; + }()), + initialize: function(){ + var CollectionType; + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + BaseView.prototype.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + return this.collection.on('reset', this.resetFields, this); + }, + addField: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + view = this.addSubview(new SubviewType({ + model: field + })); + view.on('change', this.onChange.bind(this, field)); + this.render(); + return view; + }, + resetFields: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return this; + }, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return Scaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); + +}); diff --git a/lib/chart/chart-type.js b/lib/chart/chart-type.js new file mode 100644 index 0000000..037682a --- /dev/null +++ b/lib/chart/chart-type.js @@ -0,0 +1,435 @@ +var moment, Backbone, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; +moment = require('moment'); +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_TYPES = exports.KNOWN_CHART_TYPES = {}; +/** + * @class Abstraction of a chart-type or charting library, encapsulating its + * logic and options. In addition, a `ChartType` also mediates the + * transformation of the domain-specific data types (the model and its view) + * with its specific needs. + * + * `ChartType`s mix in `ParserMixin`: when implementing a `ChartType`, you can + * add or supplement parsers merely by subclassing and overriding the + * corresponding `parseXXX` method (such as `parseArray` or `parseDate`). + * + * @extends EventEmitter + * @borrows ParserMixin + */ +exports.ChartType = ChartType = (function(superclass){ + /** + * Register a new chart type. + */ + ChartType.displayName = 'ChartType'; + var prototype = __extend(ChartType, superclass).prototype, constructor = ChartType; + ChartType.register = function(Subclass){ + return KNOWN_CHART_TYPES[Subclass.prototype.typeName] = Subclass; + }; + /** + * Look up a `ChartType` by `typeName`. + */ + ChartType.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartType'); + } + return KNOWN_CHART_TYPES[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {ChartType} + */ + ChartType.create = function(model, view){ + var Type; + if (!(Type = this.lookup(model))) { + return null; + } + return new Type(model, view); + }; + /* + * These are "class properties": each is set on the prototype at the class-level, + * and the reference is therefore shared by all instances. It is expected you + * will not modify this on the instance-level. + */ + /** + * URL for the Chart Spec JSON. Loaded once, the first time an instance of + * that class is created. + * @type String + * @readonly + */ + prototype.SPEC_URL = null; + /** + * Chart-type name. + * @type String + * @readonly + */ + prototype.typeName = null; + /** + * Map of option name to ChartOption objects. + * @type { name:ChartOption, ... } + * @readonly + */ + prototype.options = null; + /** + * Ordered ChartOption objects. + * + * This is a "class-property": it is set on the prototype at the class-level, + * and the reference is shared by all instances. It is expected you will not + * modify that instance. + * + * @type ChartOption[] + * @readonly + */ + prototype.options_ordered = null; + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport' + }; + /** + * Whether the ChartType has loaded all its data and is ready. + * @type Boolean + */ + prototype.ready = false; + /** + * Model to be rendered as a chart. + * @type Backbone.Model + */ + prototype.model = null; + /** + * View to render the chart into. + * @type Backbone.View + */ + prototype.view = null; + /** + * Last chart rendered by this ChartType. + * @private + */ + prototype.chart = null; + /** + * @constructor + */; + function ChartType(model, view){ + this.model = model; + this.view = view; + this.roles || (this.roles = {}); + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + } + prototype.withModel = function(model){ + this.model = model; + return this; + }; + prototype.withView = function(view){ + this.view = view; + return this; + }; + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + prototype.loadSpec = function(){ + var proto, _this = this; + if (this.ready) { + return this; + } + proto = this.constructor.prototype; + jQuery.ajax({ + url: this.SPEC_URL, + dataType: 'json', + success: function(spec){ + proto.spec = spec; + proto.options_ordered = spec; + proto.options = _.synthesize(spec, function(it){ + return [it.name, it]; + }); + proto.ready = true; + return _this.triggerReady(); + }, + error: function(it){ + return console.error("Error loading " + _this.typeName + " spec! " + it); + } + }); + return this; + }; + /** + * @returns {ChartOption} Get an option's spec by name. + */ + prototype.getOption = function(name, def){ + return this.options[name] || def; + }; + /** + * @returns {Object} An object, mapping from option.name to the + * result of the supplied function. + */ + prototype.map = function(fn, context){ + var _this = this; + context == null && (context = this); + return _.synthesize(this.options, function(it){ + return [it.name, fn.call(context, it, it.name, _this)]; + }); + }; + /** + * @param {String} attr Attribute to look up on each options object. + * @returns {Object} Map from name to the value found at the given attr. + */ + prototype.pluck = function(attr){ + return this.map(function(it){ + return it[attr]; + }); + }; + /** + * @returns {Boolean} Whether the supplied value is the same as + * the default value for the given key. + */ + prototype.isDefault = function(name, value){ + return _.isEqual(this.getOption(name)['default'], value); + }; + /** + * When implementing a ChartType, you can add or override parsers + * merely by subclassing. + * @borrows ParserMixin + */; + ParserMixin.mix(ChartType); + /** + * @returns {Function} Parser for the given option name. + */ + prototype.getParserFor = function(name){ + return this.getParser(this.getOption(name).type); + }; + /** + * Parses a single serialized option value into its proper type. + * + * @param {String} name Option-name of the value being parsed. + * @param {String} value Value to parse. + * @returns {*} Parsed value. + */ + prototype.parseOption = function(name, value){ + return this.getParserFor(name)(value); + }; + /** + * Parses options using `parseOption(name, value)`. + * + * @param {Object} options Options to parse. + * @returns {Object} Parsed options. + */ + prototype.parseOptions = function(options){ + var out, k, v; + out = {}; + for (k in options) { + v = options[k]; + out[k] = this.parseOption(k, v); + } + return out; + }; + /** + * Serializes option-value to a String. + * + * @param {*} v Value to serialize. + * @param {String} k Option-name of the given value. + * @returns {String} The serialized value + */ + prototype.serialize = function(v, k){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return String(v); + }; + /** + * Formats a date for display on an axis: `MM/YYYY` + * @param {Date} d Date to format. + * @returns {String} + */ + prototype.axisDateFormatter = function(d){ + return moment(d).format('MM/YYYY'); + }; + /** + * Formats a date for display in the legend: `DD MMM YYYY` + * @param {Date} d Date to format. + * @returns {String} + */ + prototype.dateFormatter = function(d){ + return moment(d).format('DD MMM YYYY'); + }; + /** + * Formats a number for display, first dividing by the greatest suffix + * of {B = Billions, M = Millions, K = Thousands} that results in a + * absolute value greater than 0, and then rounding to `digits` using + * `result.toFixed(digits)`. + * + * @param {Number} n Number to format. + * @param {Number} [digits=2] Number of digits after the decimal to always display. + * @param {Boolean} [abbrev=true] Expand number suffixes if false. + * @returns {Object} Formatted number parts. + */ + prototype.numberFormatter = function(n, digits, abbrev){ + var suffixes, suffix, d, s, parts, whole, fraction, _i, _len, _ref; + digits == null && (digits = 2); + abbrev == null && (abbrev = true); + suffixes = abbrev + ? [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + : [['Billion', 1000000000], ['Million', 1000000], ['', NaN]]; + for (_i = 0, _len = suffixes.length; _i < _len; ++_i) { + _ref = suffixes[_i], suffix = _ref[0], d = _ref[1]; + if (isNaN(d)) { + break; + } + if (n >= d) { + n = n / d; + break; + } + } + s = n.toFixed(digits); + parts = s.split('.'); + whole = _.rchop(parts[0], 3).join(','); + fraction = '.' + parts.slice(1).join('.'); + return { + n: n, + digits: digits, + whole: whole, + fraction: fraction, + suffix: suffix, + toString: function(){ + return this.whole + "" + this.fraction + (abbrev ? '' : ' ') + this.suffix; + } + }; + }; + /** + * Finds the element in the view which plays the given role in the chart. + * Canonically, all charts have a "viewport" element. Other roles might + * include a "legend" element, or several "axis" elements. + * + * Default implementation looks up a selector in the `roles` hash, and if + * found, queries the view for matching children. + * + * @param {String} role Name of the role to look up. + * @returns {jQuery|null} $-wrapped DOM element. + */ + prototype.getElementsForRole = function(role){ + var that; + if (!this.view) { + return null; + } + if (that = this.roles[role]) { + return this.view.$(that); + } else { + return null; + } + }; + /** + * Transform/extract the data for this chart from the model. Default + * implementation calls `model.getData()`. + * + * @returns {*} Data object for the chart. + */ + prototype.getData = function(){ + return this.model.getData(); + }; + /** + * Map from option-name to default value. Note that this reference will be + * modified by `.render()`. + * + * @returns {Object} Default options. + */ + prototype.getDefaultOptions = function(){ + return this.pluck('default'); + }; + /** + * Resizes the HTML viewport. Override to disable, etc. + */ + prototype.resizeViewport = function(){ + var size; + size = this.determineSize(); + this.getElementsForRole('viewport').css(size); + return size; + }; + /** + * Determines chart viewport size. + * @return { width, height } + */ + prototype.determineSize = function(){ + var width, modelW, height, modelH, viewport, Width; + modelW = width = this.model.get('width'); + modelH = height = this.model.get('height'); + if (!(this.view.ready && width && height)) { + return { + width: width, + height: height + }; + } + viewport = this.getElementsForRole('viewport'); + if (width === 'auto') { + Width = viewport.innerWidth() || 300; + } + width == null && (width = modelW); + if (height === 'auto') { + height = viewport.innerHeight() || 320; + } + height == null && (height = modelH); + return { + width: width, + height: height + }; + }; + /** + * Transforms domain data and applies it to the chart library to + * render or update the corresponding chart. + * + * @returns {Chart} + */ + prototype.render = function(){ + var data, options, viewport; + data = this.getData(); + options = __import(this.getDefaultOptions(), this.transform(this.model, this.view)); + viewport = this.getElementsForRole('viewport'); + if (!((data != null && data.length) && (viewport != null && viewport.length))) { + return this.lastChart; + } + return this.lastChart = this.renderChart(data, viewport, options, this.chart); + }; + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * + * Default implementation returns `model.getOptions()`. + * + * @returns {Object} The derived data. + */ + prototype.transform = function(model, view){ + return this.model.getOptions(); + }; + /** + * Called to render the chart. + * + * @abstract + * @returns {Chart} + */ + prototype.renderChart = function(data, viewport, options, lastChart){ + throw Error('unimplemented'); + }; + return ChartType; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/chart-type.mod.js b/lib/chart/chart-type.mod.js new file mode 100644 index 0000000..40aaea3 --- /dev/null +++ b/lib/chart/chart-type.mod.js @@ -0,0 +1,439 @@ +require.define('/node_modules/kraken/chart/chart-type.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var moment, Backbone, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; +moment = require('moment'); +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_TYPES = exports.KNOWN_CHART_TYPES = {}; +/** + * @class Abstraction of a chart-type or charting library, encapsulating its + * logic and options. In addition, a `ChartType` also mediates the + * transformation of the domain-specific data types (the model and its view) + * with its specific needs. + * + * `ChartType`s mix in `ParserMixin`: when implementing a `ChartType`, you can + * add or supplement parsers merely by subclassing and overriding the + * corresponding `parseXXX` method (such as `parseArray` or `parseDate`). + * + * @extends EventEmitter + * @borrows ParserMixin + */ +exports.ChartType = ChartType = (function(superclass){ + /** + * Register a new chart type. + */ + ChartType.displayName = 'ChartType'; + var prototype = __extend(ChartType, superclass).prototype, constructor = ChartType; + ChartType.register = function(Subclass){ + return KNOWN_CHART_TYPES[Subclass.prototype.typeName] = Subclass; + }; + /** + * Look up a `ChartType` by `typeName`. + */ + ChartType.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartType'); + } + return KNOWN_CHART_TYPES[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {ChartType} + */ + ChartType.create = function(model, view){ + var Type; + if (!(Type = this.lookup(model))) { + return null; + } + return new Type(model, view); + }; + /* + * These are "class properties": each is set on the prototype at the class-level, + * and the reference is therefore shared by all instances. It is expected you + * will not modify this on the instance-level. + */ + /** + * URL for the Chart Spec JSON. Loaded once, the first time an instance of + * that class is created. + * @type String + * @readonly + */ + prototype.SPEC_URL = null; + /** + * Chart-type name. + * @type String + * @readonly + */ + prototype.typeName = null; + /** + * Map of option name to ChartOption objects. + * @type { name:ChartOption, ... } + * @readonly + */ + prototype.options = null; + /** + * Ordered ChartOption objects. + * + * This is a "class-property": it is set on the prototype at the class-level, + * and the reference is shared by all instances. It is expected you will not + * modify that instance. + * + * @type ChartOption[] + * @readonly + */ + prototype.options_ordered = null; + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport' + }; + /** + * Whether the ChartType has loaded all its data and is ready. + * @type Boolean + */ + prototype.ready = false; + /** + * Model to be rendered as a chart. + * @type Backbone.Model + */ + prototype.model = null; + /** + * View to render the chart into. + * @type Backbone.View + */ + prototype.view = null; + /** + * Last chart rendered by this ChartType. + * @private + */ + prototype.chart = null; + /** + * @constructor + */; + function ChartType(model, view){ + this.model = model; + this.view = view; + this.roles || (this.roles = {}); + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + } + prototype.withModel = function(model){ + this.model = model; + return this; + }; + prototype.withView = function(view){ + this.view = view; + return this; + }; + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + prototype.loadSpec = function(){ + var proto, _this = this; + if (this.ready) { + return this; + } + proto = this.constructor.prototype; + jQuery.ajax({ + url: this.SPEC_URL, + dataType: 'json', + success: function(spec){ + proto.spec = spec; + proto.options_ordered = spec; + proto.options = _.synthesize(spec, function(it){ + return [it.name, it]; + }); + proto.ready = true; + return _this.triggerReady(); + }, + error: function(it){ + return console.error("Error loading " + _this.typeName + " spec! " + it); + } + }); + return this; + }; + /** + * @returns {ChartOption} Get an option's spec by name. + */ + prototype.getOption = function(name, def){ + return this.options[name] || def; + }; + /** + * @returns {Object} An object, mapping from option.name to the + * result of the supplied function. + */ + prototype.map = function(fn, context){ + var _this = this; + context == null && (context = this); + return _.synthesize(this.options, function(it){ + return [it.name, fn.call(context, it, it.name, _this)]; + }); + }; + /** + * @param {String} attr Attribute to look up on each options object. + * @returns {Object} Map from name to the value found at the given attr. + */ + prototype.pluck = function(attr){ + return this.map(function(it){ + return it[attr]; + }); + }; + /** + * @returns {Boolean} Whether the supplied value is the same as + * the default value for the given key. + */ + prototype.isDefault = function(name, value){ + return _.isEqual(this.getOption(name)['default'], value); + }; + /** + * When implementing a ChartType, you can add or override parsers + * merely by subclassing. + * @borrows ParserMixin + */; + ParserMixin.mix(ChartType); + /** + * @returns {Function} Parser for the given option name. + */ + prototype.getParserFor = function(name){ + return this.getParser(this.getOption(name).type); + }; + /** + * Parses a single serialized option value into its proper type. + * + * @param {String} name Option-name of the value being parsed. + * @param {String} value Value to parse. + * @returns {*} Parsed value. + */ + prototype.parseOption = function(name, value){ + return this.getParserFor(name)(value); + }; + /** + * Parses options using `parseOption(name, value)`. + * + * @param {Object} options Options to parse. + * @returns {Object} Parsed options. + */ + prototype.parseOptions = function(options){ + var out, k, v; + out = {}; + for (k in options) { + v = options[k]; + out[k] = this.parseOption(k, v); + } + return out; + }; + /** + * Serializes option-value to a String. + * + * @param {*} v Value to serialize. + * @param {String} k Option-name of the given value. + * @returns {String} The serialized value + */ + prototype.serialize = function(v, k){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return String(v); + }; + /** + * Formats a date for display on an axis: `MM/YYYY` + * @param {Date} d Date to format. + * @returns {String} + */ + prototype.axisDateFormatter = function(d){ + return moment(d).format('MM/YYYY'); + }; + /** + * Formats a date for display in the legend: `DD MMM YYYY` + * @param {Date} d Date to format. + * @returns {String} + */ + prototype.dateFormatter = function(d){ + return moment(d).format('DD MMM YYYY'); + }; + /** + * Formats a number for display, first dividing by the greatest suffix + * of {B = Billions, M = Millions, K = Thousands} that results in a + * absolute value greater than 0, and then rounding to `digits` using + * `result.toFixed(digits)`. + * + * @param {Number} n Number to format. + * @param {Number} [digits=2] Number of digits after the decimal to always display. + * @param {Boolean} [abbrev=true] Expand number suffixes if false. + * @returns {Object} Formatted number parts. + */ + prototype.numberFormatter = function(n, digits, abbrev){ + var suffixes, suffix, d, s, parts, whole, fraction, _i, _len, _ref; + digits == null && (digits = 2); + abbrev == null && (abbrev = true); + suffixes = abbrev + ? [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + : [['Billion', 1000000000], ['Million', 1000000], ['', NaN]]; + for (_i = 0, _len = suffixes.length; _i < _len; ++_i) { + _ref = suffixes[_i], suffix = _ref[0], d = _ref[1]; + if (isNaN(d)) { + break; + } + if (n >= d) { + n = n / d; + break; + } + } + s = n.toFixed(digits); + parts = s.split('.'); + whole = _.rchop(parts[0], 3).join(','); + fraction = '.' + parts.slice(1).join('.'); + return { + n: n, + digits: digits, + whole: whole, + fraction: fraction, + suffix: suffix, + toString: function(){ + return this.whole + "" + this.fraction + (abbrev ? '' : ' ') + this.suffix; + } + }; + }; + /** + * Finds the element in the view which plays the given role in the chart. + * Canonically, all charts have a "viewport" element. Other roles might + * include a "legend" element, or several "axis" elements. + * + * Default implementation looks up a selector in the `roles` hash, and if + * found, queries the view for matching children. + * + * @param {String} role Name of the role to look up. + * @returns {jQuery|null} $-wrapped DOM element. + */ + prototype.getElementsForRole = function(role){ + var that; + if (!this.view) { + return null; + } + if (that = this.roles[role]) { + return this.view.$(that); + } else { + return null; + } + }; + /** + * Transform/extract the data for this chart from the model. Default + * implementation calls `model.getData()`. + * + * @returns {*} Data object for the chart. + */ + prototype.getData = function(){ + return this.model.getData(); + }; + /** + * Map from option-name to default value. Note that this reference will be + * modified by `.render()`. + * + * @returns {Object} Default options. + */ + prototype.getDefaultOptions = function(){ + return this.pluck('default'); + }; + /** + * Resizes the HTML viewport. Override to disable, etc. + */ + prototype.resizeViewport = function(){ + var size; + size = this.determineSize(); + this.getElementsForRole('viewport').css(size); + return size; + }; + /** + * Determines chart viewport size. + * @return { width, height } + */ + prototype.determineSize = function(){ + var width, modelW, height, modelH, viewport, Width; + modelW = width = this.model.get('width'); + modelH = height = this.model.get('height'); + if (!(this.view.ready && width && height)) { + return { + width: width, + height: height + }; + } + viewport = this.getElementsForRole('viewport'); + if (width === 'auto') { + Width = viewport.innerWidth() || 300; + } + width == null && (width = modelW); + if (height === 'auto') { + height = viewport.innerHeight() || 320; + } + height == null && (height = modelH); + return { + width: width, + height: height + }; + }; + /** + * Transforms domain data and applies it to the chart library to + * render or update the corresponding chart. + * + * @returns {Chart} + */ + prototype.render = function(){ + var data, options, viewport; + data = this.getData(); + options = __import(this.getDefaultOptions(), this.transform(this.model, this.view)); + viewport = this.getElementsForRole('viewport'); + if (!((data != null && data.length) && (viewport != null && viewport.length))) { + return this.lastChart; + } + return this.lastChart = this.renderChart(data, viewport, options, this.chart); + }; + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * + * Default implementation returns `model.getOptions()`. + * + * @returns {Object} The derived data. + */ + prototype.transform = function(model, view){ + return this.model.getOptions(); + }; + /** + * Called to render the chart. + * + * @abstract + * @returns {Chart} + */ + prototype.renderChart = function(data, viewport, options, lastChart){ + throw Error('unimplemented'); + }; + return ChartType; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/index.js b/lib/chart/index.js new file mode 100644 index 0000000..21c43f1 --- /dev/null +++ b/lib/chart/index.js @@ -0,0 +1,12 @@ +var chart_type, chart_option, dygraphs, d3_chart, d3_elements; +chart_type = require('kraken/chart/chart-type'); +chart_option = require('kraken/chart/option'); +dygraphs = require('kraken/chart/type/dygraphs'); +d3_chart = require('kraken/chart/type/d3-chart'); +d3_elements = require('kraken/chart/type/d3'); +__import(__import(__import(__import(__import(exports, chart_type), chart_option), dygraphs), d3_chart), d3_elements); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/index.mod.js b/lib/chart/index.mod.js new file mode 100644 index 0000000..7734fba --- /dev/null +++ b/lib/chart/index.mod.js @@ -0,0 +1,16 @@ +require.define('/node_modules/kraken/chart/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var chart_type, chart_option, dygraphs, d3_chart, d3_elements; +chart_type = require('kraken/chart/chart-type'); +chart_option = require('kraken/chart/option'); +dygraphs = require('kraken/chart/type/dygraphs'); +d3_chart = require('kraken/chart/type/d3-chart'); +d3_elements = require('kraken/chart/type/d3'); +__import(__import(__import(__import(__import(exports, chart_type), chart_option), dygraphs), d3_chart), d3_elements); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/option/chart-option-model.js b/lib/chart/option/chart-option-model.js new file mode 100644 index 0000000..4b7382f --- /dev/null +++ b/lib/chart/option/chart-option-model.js @@ -0,0 +1,256 @@ +var op, Parsers, ParserMixin, ParsingModel, ParsingView, BaseModel, BaseList, TagSet, KNOWN_TAGS, ChartOption, ChartOptionList, _ref, _, __slice = [].slice; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin, ParsingModel = _ref.ParsingModel, ParsingView = _ref.ParsingView; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +/** + * @class A set of tags. + */ +exports.TagSet = TagSet = (function(superclass){ + TagSet.displayName = 'TagSet'; + var prototype = __extend(TagSet, superclass).prototype, constructor = TagSet; + prototype.tags = {}; + function TagSet(values){ + values == null && (values = []); + this.tags = {}; + if (values != null && values.length) { + this.add(values); + } + } + prototype.has = function(tag){ + return this.tags[tag] != null; + }; + prototype.get = function(tag){ + if (!tag) { + return -1; + } + if (this.tags[tag] == null) { + this.tags[tag] = this.length; + this.push(tag); + } + return this.tags[tag]; + }; + prototype.update = function(tags){ + var is_single, tag, indices, _res, _i, _len; + is_single = typeof tags === 'string'; + if (is_single) { + tags = [tags]; + } + _res = []; + for (_i = 0, _len = tags.length; _i < _len; ++_i) { + tag = tags[_i]; + _res.push(this.get(tag)); + } + indices = _res; + if (is_single) { + return indices[0]; + } else { + return indices; + } + }; + prototype.toString = function(){ + return "TagSet(length=" + this.length + ", values=[\"" + this.join('", "') + "\"])"; + }; + return TagSet; +}(Array)); +/** + * @namespace All known tags, for mapping consistently onto colors. + */ +KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet(); +/** + * @class Field with chart-option-specific handling for validation, parsing, tags, etc. + */ +ChartOption = exports.ChartOption = ParsingModel.extend({ + IGNORED_TAGS: ['callback', 'deprecated', 'debugging'], + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function ChartOption(){ + return ParsingModel.apply(this, arguments); + } + return ChartOption; + }()), + initialize: function(){ + var type, tags; + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + ChartOption.__super__.initialize.apply(this, arguments); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + KNOWN_TAGS.update(this.getCategory()); + type = this.get('type').toLowerCase() || ''; + tags = this.get('tags') || []; + if (_.str.include(type, 'function') || _.intersection(tags, this.IGNORED_TAGS).length) { + return this.set('ignore', true); + } + }, + addTag: function(tag){ + var tags; + if (!tag) { + return this; + } + tags = this.get('tags') || []; + tags.push(tag); + this.set('tags', tags); + return this; + }, + removeTag: function(tag){ + var tags; + if (!tag) { + return this; + } + tags = this.get('tags') || []; + _.remove(tags, tag); + this.set('tags', tags); + return this; + }, + onTagUpdate: function(){ + KNOWN_TAGS.update(this.get('tags')); + return this; + }, + getTagIndex: function(tag){ + return KNOWN_TAGS.get(tag); + }, + getCategory: function(){ + var tags; + return tags = (this.get('tags') || [])[0]; + }, + getCategoryIndex: function(){ + return this.getTagIndex(this.getCategory()); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serialization * * */ + /** + * Override to default `type` to the model attribute of the same name. + * @returns {Function} Parser for the given type. + */, + getParser: function(type){ + type || (type = this.get('type') || 'String'); + return ChartOption.__super__.getParser.call(this, type); + }, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +/** + * @class List of ChartOption fields. + */ +ChartOptionList = exports.ChartOptionList = BaseList.extend({ + model: ChartOption, + constructor: (function(){ + function ChartOptionList(){ + return BaseList.apply(this, arguments); + } + return ChartOptionList; + }()) + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepDefaults=true] If false, exclude pairs that + * haven't changed from their default value. + * @param {Boolean} [opts.serialize=false] If true, replace each value + * with its String version by calling `value.serializeValue()`. + * @returns {Object} Map of fields to their values. + */, + values: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + } + /** + * Override to omit defaults from URL. + */, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: false, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/option/chart-option-model.mod.js b/lib/chart/option/chart-option-model.mod.js new file mode 100644 index 0000000..50446ba --- /dev/null +++ b/lib/chart/option/chart-option-model.mod.js @@ -0,0 +1,260 @@ +require.define('/node_modules/kraken/chart/option/chart-option-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, Parsers, ParserMixin, ParsingModel, ParsingView, BaseModel, BaseList, TagSet, KNOWN_TAGS, ChartOption, ChartOptionList, _ref, _, __slice = [].slice; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin, ParsingModel = _ref.ParsingModel, ParsingView = _ref.ParsingView; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +/** + * @class A set of tags. + */ +exports.TagSet = TagSet = (function(superclass){ + TagSet.displayName = 'TagSet'; + var prototype = __extend(TagSet, superclass).prototype, constructor = TagSet; + prototype.tags = {}; + function TagSet(values){ + values == null && (values = []); + this.tags = {}; + if (values != null && values.length) { + this.add(values); + } + } + prototype.has = function(tag){ + return this.tags[tag] != null; + }; + prototype.get = function(tag){ + if (!tag) { + return -1; + } + if (this.tags[tag] == null) { + this.tags[tag] = this.length; + this.push(tag); + } + return this.tags[tag]; + }; + prototype.update = function(tags){ + var is_single, tag, indices, _res, _i, _len; + is_single = typeof tags === 'string'; + if (is_single) { + tags = [tags]; + } + _res = []; + for (_i = 0, _len = tags.length; _i < _len; ++_i) { + tag = tags[_i]; + _res.push(this.get(tag)); + } + indices = _res; + if (is_single) { + return indices[0]; + } else { + return indices; + } + }; + prototype.toString = function(){ + return "TagSet(length=" + this.length + ", values=[\"" + this.join('", "') + "\"])"; + }; + return TagSet; +}(Array)); +/** + * @namespace All known tags, for mapping consistently onto colors. + */ +KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet(); +/** + * @class Field with chart-option-specific handling for validation, parsing, tags, etc. + */ +ChartOption = exports.ChartOption = ParsingModel.extend({ + IGNORED_TAGS: ['callback', 'deprecated', 'debugging'], + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function ChartOption(){ + return ParsingModel.apply(this, arguments); + } + return ChartOption; + }()), + initialize: function(){ + var type, tags; + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + ChartOption.__super__.initialize.apply(this, arguments); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + KNOWN_TAGS.update(this.getCategory()); + type = this.get('type').toLowerCase() || ''; + tags = this.get('tags') || []; + if (_.str.include(type, 'function') || _.intersection(tags, this.IGNORED_TAGS).length) { + return this.set('ignore', true); + } + }, + addTag: function(tag){ + var tags; + if (!tag) { + return this; + } + tags = this.get('tags') || []; + tags.push(tag); + this.set('tags', tags); + return this; + }, + removeTag: function(tag){ + var tags; + if (!tag) { + return this; + } + tags = this.get('tags') || []; + _.remove(tags, tag); + this.set('tags', tags); + return this; + }, + onTagUpdate: function(){ + KNOWN_TAGS.update(this.get('tags')); + return this; + }, + getTagIndex: function(tag){ + return KNOWN_TAGS.get(tag); + }, + getCategory: function(){ + var tags; + return tags = (this.get('tags') || [])[0]; + }, + getCategoryIndex: function(){ + return this.getTagIndex(this.getCategory()); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serialization * * */ + /** + * Override to default `type` to the model attribute of the same name. + * @returns {Function} Parser for the given type. + */, + getParser: function(type){ + type || (type = this.get('type') || 'String'); + return ChartOption.__super__.getParser.call(this, type); + }, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +/** + * @class List of ChartOption fields. + */ +ChartOptionList = exports.ChartOptionList = BaseList.extend({ + model: ChartOption, + constructor: (function(){ + function ChartOptionList(){ + return BaseList.apply(this, arguments); + } + return ChartOptionList; + }()) + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepDefaults=true] If false, exclude pairs that + * haven't changed from their default value. + * @param {Boolean} [opts.serialize=false] If true, replace each value + * with its String version by calling `value.serializeValue()`. + * @returns {Object} Map of fields to their values. + */, + values: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + } + /** + * Override to omit defaults from URL. + */, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: false, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/option/chart-option-view.js b/lib/chart/option/chart-option-view.js new file mode 100644 index 0000000..99a9a41 --- /dev/null +++ b/lib/chart/option/chart-option-view.js @@ -0,0 +1,256 @@ +var op, BaseView, ChartOption, ChartOptionList, DEBOUNCE_RENDER, ChartOptionView, ChartOptionScaffold, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/chart/option/chart-option-model'), ChartOption = _ref.ChartOption, ChartOptionList = _ref.ChartOptionList; +DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100; +/** + * @class View for a single configurable option in a chart type. + */ +ChartOptionView = exports.ChartOptionView = BaseView.extend({ + tagName: 'section', + className: 'chart-option field', + template: require('kraken/template/chart/chart-option'), + type: 'string', + isCollapsed: true, + events: { + 'blur .value': 'onChange', + 'click input[type="checkbox"].value': 'onChange', + 'submit .value': 'onChange', + 'click .close': 'toggleCollapsed', + 'click h3': 'toggleCollapsed', + 'click .collapsed': 'onClick' + }, + constructor: (function(){ + function ChartOptionView(){ + return BaseView.apply(this, arguments); + } + return ChartOptionView; + }()), + initialize: function(){ + ChartOptionView.__super__.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + } + /* * * * Rendering * * * */, + toTemplateLocals: function(){ + var json, v; + json = ChartOptionView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + v = json.value; + if (v && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return json; + } + /** + * Override to annotate with collapsed state and to kill off ignored options + * so they do not contribute their values when looking at form updates. + */, + render: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + ChartOptionView.__super__.render.apply(this, arguments); + if (this.isCollapsed) { + this.$el.addClass('collapsed'); + } + return this; + } + /* * * * Option Collapsing * * * */ + /** + * Sets the state of `isCollapsed` and updates the UI. If the state changed, + * a `'change:collapse`` event will be fired.` + * + * @param {Boolean} [makeCollapsed=true] If true, set state to collapsed. + * @returns {Boolean} Whether the state changed. + */, + collapse: function(state){ + state == null && (state = true); + state = !!state; + this.isCollapsed = this.$el.hasClass('collapsed'); + if (state === this.isCollapsed) { + return this; + } + if (state) { + this.$el.addClass('collapsed'); + } else { + this.$el.removeClass('collapsed'); + } + this.isCollapsed = state; + this.trigger('change:collapse', this, this.isCollapsed); + return true; + } + /** + * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event. + * @returns {this} + */, + toggleCollapsed: function(){ + this.collapse(!this.$el.hasClass('collapsed')); + return this; + } + /* * * * Events * * * */ + /** + * To prevent `toggleCollapsed()` from being called multiple times due to + * overlapping listeners, we're only looking for clicks on the collapsed header. + */, + onClick: function(evt){ + var target; + target = $(evt.target); + if (this.$el.hasClass('collapsed') && !target.hasClass('close')) { + return this.toggleCollapsed(); + } + } + /** + * Propagate user input changes to the model, and upward to the parent view. + */, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + console.log(this + ".onChange( " + current + " -> " + val + " )"); + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this.model, this); + } +}); +/** + * @class View for configuring a chart type. + */ +ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend({ + __bind__: ['addField'], + tagName: 'form', + className: 'chart-options scaffold', + template: require('kraken/template/chart/chart-scaffold'), + collectionType: ChartOptionList, + subviewType: ChartOptionView, + events: { + 'click .options-filter-button': 'onFilterOptions', + 'click .collapse-all-options-button': 'collapseAll', + 'click .expand-all-options-button': 'expandAll' + }, + constructor: (function(){ + function ChartOptionScaffold(){ + return BaseView.apply(this, arguments); + } + return ChartOptionScaffold; + }()), + initialize: function(){ + var CollectionType; + this.render = _.debounce(this.render.bind(this), DEBOUNCE_RENDER); + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + ChartOptionScaffold.__super__.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + this.collection.on('reset', this.onReset, this); + return this.on('render', this.onRender, this); + } + /** + * Bookkeeping for new ChartOptions, creating it a new subview and subscribing + * to its activity, and then rendering it. + * @returns {ChartOptionView} The Option's new view. + */, + addField: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + this.addSubview(view = new SubviewType({ + model: field + })).on('change', this.onChange.bind(this, field)).on('change:collapse', this.render, this); + this.render(); + return view; + } + /* * * * UI * * * */ + /** + * Collapse all expanded subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */, + collapseAll: function(){ + _.invoke(this.subviews, 'collapse', true); + return false; + } + /** + * Expand all collapsed subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */, + expandAll: function(){ + _.invoke(this.subviews, 'collapse', false); + return false; + } + /** + * Reflow Isotope post-`render()`. + */, + onRender: function(){ + if (!this.$el.is(':visible')) { + return; + } + return this.$('.isotope').isotope({ + itemSelector: '.chart-option.field', + layoutMode: 'masonry', + masonry: { + columnWidth: 10 + }, + filter: this.getOptionsFilter(), + sortBy: 'category', + getSortData: { + category: function($el){ + return $el.data('model').getCategory(); + } + } + }); + } + /** + * @returns {String} Selector representing the selected set of Option filters. + */, + getOptionsFilter: function(){ + var data, sel; + data = this.$('.options-filter-button.active').toArray().map(function(it){ + return $(it).data(); + }); + sel = data.reduce(function(sel, d){ + var that; + return sel += (that = d.filter) ? that : ''; + }, ':not(.ignore)'); + return sel; + } + /* * * * Events * * * */ + /** + * Propagate change events from fields as if they were attribute changes. + * Note: `field` is bound to the handler + */, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + }, + onReset: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return _.defer(this.render); + }, + onFilterOptions: function(evt){ + evt.preventDefault(); + return _.defer(this.render); + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return ChartOptionScaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); \ No newline at end of file diff --git a/lib/chart/option/chart-option-view.mod.js b/lib/chart/option/chart-option-view.mod.js new file mode 100644 index 0000000..8bf441a --- /dev/null +++ b/lib/chart/option/chart-option-view.mod.js @@ -0,0 +1,260 @@ +require.define('/node_modules/kraken/chart/option/chart-option-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseView, ChartOption, ChartOptionList, DEBOUNCE_RENDER, ChartOptionView, ChartOptionScaffold, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/chart/option/chart-option-model'), ChartOption = _ref.ChartOption, ChartOptionList = _ref.ChartOptionList; +DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100; +/** + * @class View for a single configurable option in a chart type. + */ +ChartOptionView = exports.ChartOptionView = BaseView.extend({ + tagName: 'section', + className: 'chart-option field', + template: require('kraken/template/chart/chart-option'), + type: 'string', + isCollapsed: true, + events: { + 'blur .value': 'onChange', + 'click input[type="checkbox"].value': 'onChange', + 'submit .value': 'onChange', + 'click .close': 'toggleCollapsed', + 'click h3': 'toggleCollapsed', + 'click .collapsed': 'onClick' + }, + constructor: (function(){ + function ChartOptionView(){ + return BaseView.apply(this, arguments); + } + return ChartOptionView; + }()), + initialize: function(){ + ChartOptionView.__super__.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + } + /* * * * Rendering * * * */, + toTemplateLocals: function(){ + var json, v; + json = ChartOptionView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + v = json.value; + if (v && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return json; + } + /** + * Override to annotate with collapsed state and to kill off ignored options + * so they do not contribute their values when looking at form updates. + */, + render: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + ChartOptionView.__super__.render.apply(this, arguments); + if (this.isCollapsed) { + this.$el.addClass('collapsed'); + } + return this; + } + /* * * * Option Collapsing * * * */ + /** + * Sets the state of `isCollapsed` and updates the UI. If the state changed, + * a `'change:collapse`` event will be fired.` + * + * @param {Boolean} [makeCollapsed=true] If true, set state to collapsed. + * @returns {Boolean} Whether the state changed. + */, + collapse: function(state){ + state == null && (state = true); + state = !!state; + this.isCollapsed = this.$el.hasClass('collapsed'); + if (state === this.isCollapsed) { + return this; + } + if (state) { + this.$el.addClass('collapsed'); + } else { + this.$el.removeClass('collapsed'); + } + this.isCollapsed = state; + this.trigger('change:collapse', this, this.isCollapsed); + return true; + } + /** + * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event. + * @returns {this} + */, + toggleCollapsed: function(){ + this.collapse(!this.$el.hasClass('collapsed')); + return this; + } + /* * * * Events * * * */ + /** + * To prevent `toggleCollapsed()` from being called multiple times due to + * overlapping listeners, we're only looking for clicks on the collapsed header. + */, + onClick: function(evt){ + var target; + target = $(evt.target); + if (this.$el.hasClass('collapsed') && !target.hasClass('close')) { + return this.toggleCollapsed(); + } + } + /** + * Propagate user input changes to the model, and upward to the parent view. + */, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + console.log(this + ".onChange( " + current + " -> " + val + " )"); + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this.model, this); + } +}); +/** + * @class View for configuring a chart type. + */ +ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend({ + __bind__: ['addField'], + tagName: 'form', + className: 'chart-options scaffold', + template: require('kraken/template/chart/chart-scaffold'), + collectionType: ChartOptionList, + subviewType: ChartOptionView, + events: { + 'click .options-filter-button': 'onFilterOptions', + 'click .collapse-all-options-button': 'collapseAll', + 'click .expand-all-options-button': 'expandAll' + }, + constructor: (function(){ + function ChartOptionScaffold(){ + return BaseView.apply(this, arguments); + } + return ChartOptionScaffold; + }()), + initialize: function(){ + var CollectionType; + this.render = _.debounce(this.render.bind(this), DEBOUNCE_RENDER); + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + ChartOptionScaffold.__super__.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + this.collection.on('reset', this.onReset, this); + return this.on('render', this.onRender, this); + } + /** + * Bookkeeping for new ChartOptions, creating it a new subview and subscribing + * to its activity, and then rendering it. + * @returns {ChartOptionView} The Option's new view. + */, + addField: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + this.addSubview(view = new SubviewType({ + model: field + })).on('change', this.onChange.bind(this, field)).on('change:collapse', this.render, this); + this.render(); + return view; + } + /* * * * UI * * * */ + /** + * Collapse all expanded subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */, + collapseAll: function(){ + _.invoke(this.subviews, 'collapse', true); + return false; + } + /** + * Expand all collapsed subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */, + expandAll: function(){ + _.invoke(this.subviews, 'collapse', false); + return false; + } + /** + * Reflow Isotope post-`render()`. + */, + onRender: function(){ + if (!this.$el.is(':visible')) { + return; + } + return this.$('.isotope').isotope({ + itemSelector: '.chart-option.field', + layoutMode: 'masonry', + masonry: { + columnWidth: 10 + }, + filter: this.getOptionsFilter(), + sortBy: 'category', + getSortData: { + category: function($el){ + return $el.data('model').getCategory(); + } + } + }); + } + /** + * @returns {String} Selector representing the selected set of Option filters. + */, + getOptionsFilter: function(){ + var data, sel; + data = this.$('.options-filter-button.active').toArray().map(function(it){ + return $(it).data(); + }); + sel = data.reduce(function(sel, d){ + var that; + return sel += (that = d.filter) ? that : ''; + }, ':not(.ignore)'); + return sel; + } + /* * * * Events * * * */ + /** + * Propagate change events from fields as if they were attribute changes. + * Note: `field` is bound to the handler + */, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + }, + onReset: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return _.defer(this.render); + }, + onFilterOptions: function(evt){ + evt.preventDefault(); + return _.defer(this.render); + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return ChartOptionScaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); + +}); diff --git a/lib/chart/option/index.js b/lib/chart/option/index.js new file mode 100644 index 0000000..09d95b7 --- /dev/null +++ b/lib/chart/option/index.js @@ -0,0 +1,9 @@ +var model, view; +model = require('kraken/chart/option/chart-option-model'); +view = require('kraken/chart/option/chart-option-view'); +__import(__import(exports, model), view); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/option/index.mod.js b/lib/chart/option/index.mod.js new file mode 100644 index 0000000..15c866d --- /dev/null +++ b/lib/chart/option/index.mod.js @@ -0,0 +1,13 @@ +require.define('/node_modules/kraken/chart/option/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var model, view; +model = require('kraken/chart/option/chart-option-model'); +view = require('kraken/chart/option/chart-option-view'); +__import(__import(exports, model), view); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/type/d3-chart.js b/lib/chart/type/d3-chart.js new file mode 100644 index 0000000..adce99c --- /dev/null +++ b/lib/chart/type/d3-chart.js @@ -0,0 +1,92 @@ +var d3, ColorBrewer, op, ChartType, D3ChartElement, root, D3ChartType, _ref, _; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ChartType = require('kraken/chart/chart-type').ChartType; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +root = function(){ + return this; +}(); +exports.D3ChartType = D3ChartType = (function(superclass){ + D3ChartType.displayName = 'D3ChartType'; + var prototype = __extend(D3ChartType, superclass).prototype, constructor = D3ChartType; + prototype.__bind__ = ['determineSize']; + prototype.SPEC_URL = '/schema/d3/d3-chart.json'; + prototype.typeName = 'd3-chart'; + ChartType.register(D3ChartType); + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport', + legend: '.graph-legend' + }; + function D3ChartType(){ + superclass.apply(this, arguments); + } + prototype.getData = function(){ + return this.model.dataset.getColumns(); + }; + prototype.transform = function(){ + var dataset, options; + dataset = this.model.dataset; + options = __import(this.model.getOptions(), this.determineSize()); + __import(options, { + colors: dataset.getColors(), + labels: dataset.getLabels() + }); + return options; + }; + prototype.renderChart = function(data, viewport, options, lastChart){ + var margin, width, height, xScale, yScale, dates, cols, allValues, svg, enterFrame, frame, xAxis, metrics; + margin = { + top: 20, + right: 20, + bottom: 20, + left: 20 + }; + width = 760 - margin.left - margin.right; + height = 320 - margin.top - margin.bottom; + xScale = d3.time.scale(); + yScale = d3.scale.linear(); + dates = data[0]; + cols = data.slice(1); + allValues = d3.merge(cols); + xScale.domain(d3.extent(dates)).range([0, width]); + yScale.domain(d3.extent(allValues)).range([height, 0]); + svg = d3.select(viewport[0]).selectAll("svg").remove(); + svg = d3.select(viewport[0]).selectAll("svg").data([cols]); + enterFrame = svg.enter().append("svg").append("g").attr("class", "frame"); + svg.attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom); + frame = svg.select("g.frame").attr("transform", "translate(" + margin.left + "," + margin.top + ")").attr("width", width).attr("height", height); + enterFrame.append("g").attr("class", "x axis time"); + xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0); + frame.select(".x.axis.time").attr("transform", "translate(0," + yScale.range()[0] + ")").call(xAxis); + metrics = frame.selectAll("metric").data(this.model.dataset.metrics.models); + metrics.enter().append("g").attr("class", function(d){ + return "g metric line " + d.get('label'); + }).each(function(d){ + var chartElement, chEl; + chartElement = d.get("chartElement"); + chartElement == null && (chartElement = 'd3-line'); + chEl = D3ChartElement.create(chartElement); + return chEl.renderChartElement(d, frame, xScale, yScale); + }); + metrics.exit().remove(); + return svg; + }; + return D3ChartType; +}(ChartType)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/type/d3-chart.mod.js b/lib/chart/type/d3-chart.mod.js new file mode 100644 index 0000000..f6817e0 --- /dev/null +++ b/lib/chart/type/d3-chart.mod.js @@ -0,0 +1,96 @@ +require.define('/node_modules/kraken/chart/type/d3-chart.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3, ColorBrewer, op, ChartType, D3ChartElement, root, D3ChartType, _ref, _; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ChartType = require('kraken/chart/chart-type').ChartType; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +root = function(){ + return this; +}(); +exports.D3ChartType = D3ChartType = (function(superclass){ + D3ChartType.displayName = 'D3ChartType'; + var prototype = __extend(D3ChartType, superclass).prototype, constructor = D3ChartType; + prototype.__bind__ = ['determineSize']; + prototype.SPEC_URL = '/schema/d3/d3-chart.json'; + prototype.typeName = 'd3-chart'; + ChartType.register(D3ChartType); + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport', + legend: '.graph-legend' + }; + function D3ChartType(){ + superclass.apply(this, arguments); + } + prototype.getData = function(){ + return this.model.dataset.getColumns(); + }; + prototype.transform = function(){ + var dataset, options; + dataset = this.model.dataset; + options = __import(this.model.getOptions(), this.determineSize()); + __import(options, { + colors: dataset.getColors(), + labels: dataset.getLabels() + }); + return options; + }; + prototype.renderChart = function(data, viewport, options, lastChart){ + var margin, width, height, xScale, yScale, dates, cols, allValues, svg, enterFrame, frame, xAxis, metrics; + margin = { + top: 20, + right: 20, + bottom: 20, + left: 20 + }; + width = 760 - margin.left - margin.right; + height = 320 - margin.top - margin.bottom; + xScale = d3.time.scale(); + yScale = d3.scale.linear(); + dates = data[0]; + cols = data.slice(1); + allValues = d3.merge(cols); + xScale.domain(d3.extent(dates)).range([0, width]); + yScale.domain(d3.extent(allValues)).range([height, 0]); + svg = d3.select(viewport[0]).selectAll("svg").remove(); + svg = d3.select(viewport[0]).selectAll("svg").data([cols]); + enterFrame = svg.enter().append("svg").append("g").attr("class", "frame"); + svg.attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom); + frame = svg.select("g.frame").attr("transform", "translate(" + margin.left + "," + margin.top + ")").attr("width", width).attr("height", height); + enterFrame.append("g").attr("class", "x axis time"); + xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0); + frame.select(".x.axis.time").attr("transform", "translate(0," + yScale.range()[0] + ")").call(xAxis); + metrics = frame.selectAll("metric").data(this.model.dataset.metrics.models); + metrics.enter().append("g").attr("class", function(d){ + return "g metric line " + d.get('label'); + }).each(function(d){ + var chartElement, chEl; + chartElement = d.get("chartElement"); + chartElement == null && (chartElement = 'd3-line'); + chEl = D3ChartElement.create(chartElement); + return chEl.renderChartElement(d, frame, xScale, yScale); + }); + metrics.exit().remove(); + return svg; + }; + return D3ChartType; +}(ChartType)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/type/d3/d3-bar-element.js b/lib/chart/type/d3/d3-bar-element.js new file mode 100644 index 0000000..b1248b2 --- /dev/null +++ b/lib/chart/type/d3/d3-bar-element.js @@ -0,0 +1,51 @@ +var d3, op, D3ChartElement, root, BarChartType, _ref, _, _fmt; +d3 = require('d3'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +_fmt = require('kraken/util/formatters'); +root = function(){ + return this; +}(); +exports.BarChartType = BarChartType = (function(superclass){ + BarChartType.displayName = 'BarChartType'; + var prototype = __extend(BarChartType, superclass).prototype, constructor = BarChartType; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-bar.json'; + prototype.chartElement = 'd3-bar'; + D3ChartElement.register(BarChartType); + function BarChartType(){ + superclass.apply(this, arguments); + } + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + var X, Y, metricBars, data, barWidth, barHeight, chT; + X = function(d, i){ + return xScale(d[0]); + }; + Y = function(d, i){ + return yScale(d[1]); + }; + metricBars = root.metricBars = svgEl.append("g").attr("class", "metric bars " + metric.get('label')); + data = d3.zip(metric.getDateColumn(), metric.getData()); + barWidth = svgEl.attr('width') / data.length; + barHeight = function(d){ + return svgEl.attr('height') - Y(d); + }; + metricBars.selectAll("bar").data(data).enter().append("rect").attr("class", function(d, i){ + return "metric bar " + i; + }).attr("x", X).attr("y", Y).attr("height", barHeight).attr("width", barWidth).attr("fill", metric.get('color')).attr("stroke", "white").style("opacity", "0.4").style("z-index", -10); + chT = this; + metricBars.selectAll(".metric.bar").on("mouseover", function(d, i){ + return svgEl.append("text").attr("class", "mf").attr("dx", 50).attr("dy", 100).style("font-size", "0px").transition().duration(800).text("Uh boy, the target would be: " + _fmt.numberFormatter(d[1]).toString()).style("font-size", "25px"); + }).on("mouseout", function(d, i){ + return svgEl.selectAll(".mf").transition().duration(300).text("BUMMER!!!").style("font-size", "0px").remove(); + }); + return svgEl; + }; + return BarChartType; +}(D3ChartElement)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/chart/type/d3/d3-bar-element.mod.js b/lib/chart/type/d3/d3-bar-element.mod.js new file mode 100644 index 0000000..cd8d26a --- /dev/null +++ b/lib/chart/type/d3/d3-bar-element.mod.js @@ -0,0 +1,55 @@ +require.define('/node_modules/kraken/chart/type/d3/d3-bar-element.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3, op, D3ChartElement, root, BarChartType, _ref, _, _fmt; +d3 = require('d3'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +_fmt = require('kraken/util/formatters'); +root = function(){ + return this; +}(); +exports.BarChartType = BarChartType = (function(superclass){ + BarChartType.displayName = 'BarChartType'; + var prototype = __extend(BarChartType, superclass).prototype, constructor = BarChartType; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-bar.json'; + prototype.chartElement = 'd3-bar'; + D3ChartElement.register(BarChartType); + function BarChartType(){ + superclass.apply(this, arguments); + } + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + var X, Y, metricBars, data, barWidth, barHeight, chT; + X = function(d, i){ + return xScale(d[0]); + }; + Y = function(d, i){ + return yScale(d[1]); + }; + metricBars = root.metricBars = svgEl.append("g").attr("class", "metric bars " + metric.get('label')); + data = d3.zip(metric.getDateColumn(), metric.getData()); + barWidth = svgEl.attr('width') / data.length; + barHeight = function(d){ + return svgEl.attr('height') - Y(d); + }; + metricBars.selectAll("bar").data(data).enter().append("rect").attr("class", function(d, i){ + return "metric bar " + i; + }).attr("x", X).attr("y", Y).attr("height", barHeight).attr("width", barWidth).attr("fill", metric.get('color')).attr("stroke", "white").style("opacity", "0.4").style("z-index", -10); + chT = this; + metricBars.selectAll(".metric.bar").on("mouseover", function(d, i){ + return svgEl.append("text").attr("class", "mf").attr("dx", 50).attr("dy", 100).style("font-size", "0px").transition().duration(800).text("Uh boy, the target would be: " + _fmt.numberFormatter(d[1]).toString()).style("font-size", "25px"); + }).on("mouseout", function(d, i){ + return svgEl.selectAll(".mf").transition().duration(300).text("BUMMER!!!").style("font-size", "0px").remove(); + }); + return svgEl; + }; + return BarChartType; +}(D3ChartElement)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/chart/type/d3/d3-chart-element.js b/lib/chart/type/d3/d3-chart-element.js new file mode 100644 index 0000000..d61b091 --- /dev/null +++ b/lib/chart/type/d3/d3-chart-element.js @@ -0,0 +1,91 @@ +var d3, ColorBrewer, op, ReadyEmitter, root, KNOWN_CHART_ELEMENTS, D3ChartElement, _ref, _, __slice = [].slice; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +root = function(){ + return this; +}(); +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_ELEMENTS = exports.KNOWN_CHART_ELEMENTS = {}; +exports.D3ChartElement = D3ChartElement = (function(superclass){ + D3ChartElement.displayName = 'D3ChartElement'; + var prototype = __extend(D3ChartElement, superclass).prototype, constructor = D3ChartElement; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-chart.json'; + /** + * Register a new d3 element + */; + D3ChartElement.register = function(Subclass){ + return KNOWN_CHART_ELEMENTS[Subclass.prototype.chartElement] = Subclass; + }; + /** + * Look up a `charttype` by `typeName`. + */ + D3ChartElement.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartElement'); + } + return KNOWN_CHART_ELEMENTS[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {D3ChartElement} + */ + D3ChartElement.create = function(name){ + var Type; + if (!(Type = this.lookup(name))) { + return null; + } + return new Type; + }; + function D3ChartElement(){ + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + superclass.apply(this, arguments); + } + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + prototype.loadSpec = function(){ + var proto, _this = this; + if (this.ready) { + return this; + } + proto = this.constructor.prototype; + jQuery.ajax({ + url: this.SPEC_URL, + dataType: 'json', + success: function(spec){ + proto.spec = spec; + proto.options_ordered = spec; + proto.options = _.synthesize(spec, function(it){ + return [it.name, it]; + }); + proto.ready = true; + return _this.triggerReady(); + }, + error: function(it){ + return console.error("Error loading " + _this.typeName + " spec! " + it); + } + }); + return this; + }; + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + return svgEl; + }; + return D3ChartElement; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/chart/type/d3/d3-chart-element.mod.js b/lib/chart/type/d3/d3-chart-element.mod.js new file mode 100644 index 0000000..4a9e29e --- /dev/null +++ b/lib/chart/type/d3/d3-chart-element.mod.js @@ -0,0 +1,95 @@ +require.define('/node_modules/kraken/chart/type/d3/d3-chart-element.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3, ColorBrewer, op, ReadyEmitter, root, KNOWN_CHART_ELEMENTS, D3ChartElement, _ref, _, __slice = [].slice; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +root = function(){ + return this; +}(); +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_ELEMENTS = exports.KNOWN_CHART_ELEMENTS = {}; +exports.D3ChartElement = D3ChartElement = (function(superclass){ + D3ChartElement.displayName = 'D3ChartElement'; + var prototype = __extend(D3ChartElement, superclass).prototype, constructor = D3ChartElement; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-chart.json'; + /** + * Register a new d3 element + */; + D3ChartElement.register = function(Subclass){ + return KNOWN_CHART_ELEMENTS[Subclass.prototype.chartElement] = Subclass; + }; + /** + * Look up a `charttype` by `typeName`. + */ + D3ChartElement.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartElement'); + } + return KNOWN_CHART_ELEMENTS[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {D3ChartElement} + */ + D3ChartElement.create = function(name){ + var Type; + if (!(Type = this.lookup(name))) { + return null; + } + return new Type; + }; + function D3ChartElement(){ + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + superclass.apply(this, arguments); + } + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + prototype.loadSpec = function(){ + var proto, _this = this; + if (this.ready) { + return this; + } + proto = this.constructor.prototype; + jQuery.ajax({ + url: this.SPEC_URL, + dataType: 'json', + success: function(spec){ + proto.spec = spec; + proto.options_ordered = spec; + proto.options = _.synthesize(spec, function(it){ + return [it.name, it]; + }); + proto.ready = true; + return _this.triggerReady(); + }, + error: function(it){ + return console.error("Error loading " + _this.typeName + " spec! " + it); + } + }); + return this; + }; + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + return svgEl; + }; + return D3ChartElement; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/chart/type/d3/d3-line-element.js b/lib/chart/type/d3/d3-line-element.js new file mode 100644 index 0000000..c6c5867 --- /dev/null +++ b/lib/chart/type/d3/d3-line-element.js @@ -0,0 +1,59 @@ +var d3, ColorBrewer, op, D3ChartElement, root, LineChartElement, _ref, _, _fmt; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +_fmt = require('kraken/util/formatters'); +root = function(){ + return this; +}(); +exports.LineChartElement = LineChartElement = (function(superclass){ + LineChartElement.displayName = 'LineChartElement'; + var prototype = __extend(LineChartElement, superclass).prototype, constructor = LineChartElement; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-line.json'; + prototype.chartElement = 'd3-line'; + D3ChartElement.register(LineChartElement); + function LineChartElement(){ + superclass.apply(this, arguments); + } + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + var X, Y, line, metricLine, data, lens, gLens, gInner; + X = function(d, i){ + return xScale(d[0]); + }; + Y = function(d, i){ + return yScale(d[1]); + }; + line = d3.svg.line().x(X).y(Y); + metricLine = root.metricLine = svgEl.append("g").attr("class", "g metric line " + metric.get('label')); + data = d3.zip(metric.getDateColumn(), metric.getData()); + metricLine.selectAll("path.line").data(d3.zip(data.slice(0, -1), data.slice(1))).enter().append("path").attr("d", line).attr("class", function(d, i){ + return "metric line segment " + i; + }).style("stroke", metric.getColor('color')); + lens = root.lens = svgEl.selectAll("g.lens").data([[]]); + gLens = lens.enter().append("g").attr("class", "lens").style("z-index", 1e9); + gInner = gLens.append("g").attr("transform", "translate(1.5em,0)"); + gInner.append("circle").attr("r", "1.5em").style("fill", "rgba(255, 255, 255, 0.4)").style("stroke", "white").style("stroke-width", "3px"); + gInner.append("text").attr("y", "0.5em").attr("text-anchor", "middle").style("fill", "black").style("font", "12px Helvetica").style("font-weight", "bold"); + metricLine.selectAll(".line.segment").on("mouseover", function(d, i){ + var color, r, g, b, lineX, lineY, lens, _ref; + _ref = color = d3.rgb(metric.getColor('color')), r = _ref.r, g = _ref.g, b = _ref.b; + lineX = (X(d[0]) + X(d[1])) / 2; + lineY = (Y(d[0]) + Y(d[1])) / 2; + lens = svgEl.select("g.lens").attr("transform", "translate(" + lineX + ", " + lineY + ")"); + lens.select("circle").style("fill", "rgba(" + r + ", " + g + ", " + b + ", 0.4)"); + return lens.select("text").text(function(){ + return _fmt.numberFormatter(d[0][1]).toString(); + }); + }); + return svgEl; + }; + return LineChartElement; +}(D3ChartElement)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/chart/type/d3/d3-line-element.mod.js b/lib/chart/type/d3/d3-line-element.mod.js new file mode 100644 index 0000000..c0458f7 --- /dev/null +++ b/lib/chart/type/d3/d3-line-element.mod.js @@ -0,0 +1,63 @@ +require.define('/node_modules/kraken/chart/type/d3/d3-line-element.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3, ColorBrewer, op, D3ChartElement, root, LineChartElement, _ref, _, _fmt; +d3 = require('d3'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +D3ChartElement = require('kraken/chart/type/d3/d3-chart-element').D3ChartElement; +_fmt = require('kraken/util/formatters'); +root = function(){ + return this; +}(); +exports.LineChartElement = LineChartElement = (function(superclass){ + LineChartElement.displayName = 'LineChartElement'; + var prototype = __extend(LineChartElement, superclass).prototype, constructor = LineChartElement; + prototype.__bind__ = []; + prototype.SPEC_URL = '/schema/d3/d3-line.json'; + prototype.chartElement = 'd3-line'; + D3ChartElement.register(LineChartElement); + function LineChartElement(){ + superclass.apply(this, arguments); + } + prototype.renderChartElement = function(metric, svgEl, xScale, yScale){ + var X, Y, line, metricLine, data, lens, gLens, gInner; + X = function(d, i){ + return xScale(d[0]); + }; + Y = function(d, i){ + return yScale(d[1]); + }; + line = d3.svg.line().x(X).y(Y); + metricLine = root.metricLine = svgEl.append("g").attr("class", "g metric line " + metric.get('label')); + data = d3.zip(metric.getDateColumn(), metric.getData()); + metricLine.selectAll("path.line").data(d3.zip(data.slice(0, -1), data.slice(1))).enter().append("path").attr("d", line).attr("class", function(d, i){ + return "metric line segment " + i; + }).style("stroke", metric.getColor('color')); + lens = root.lens = svgEl.selectAll("g.lens").data([[]]); + gLens = lens.enter().append("g").attr("class", "lens").style("z-index", 1e9); + gInner = gLens.append("g").attr("transform", "translate(1.5em,0)"); + gInner.append("circle").attr("r", "1.5em").style("fill", "rgba(255, 255, 255, 0.4)").style("stroke", "white").style("stroke-width", "3px"); + gInner.append("text").attr("y", "0.5em").attr("text-anchor", "middle").style("fill", "black").style("font", "12px Helvetica").style("font-weight", "bold"); + metricLine.selectAll(".line.segment").on("mouseover", function(d, i){ + var color, r, g, b, lineX, lineY, lens, _ref; + _ref = color = d3.rgb(metric.getColor('color')), r = _ref.r, g = _ref.g, b = _ref.b; + lineX = (X(d[0]) + X(d[1])) / 2; + lineY = (Y(d[0]) + Y(d[1])) / 2; + lens = svgEl.select("g.lens").attr("transform", "translate(" + lineX + ", " + lineY + ")"); + lens.select("circle").style("fill", "rgba(" + r + ", " + g + ", " + b + ", 0.4)"); + return lens.select("text").text(function(){ + return _fmt.numberFormatter(d[0][1]).toString(); + }); + }); + return svgEl; + }; + return LineChartElement; +}(D3ChartElement)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/chart/type/d3/index.js b/lib/chart/type/d3/index.js new file mode 100644 index 0000000..c1193b7 --- /dev/null +++ b/lib/chart/type/d3/index.js @@ -0,0 +1,10 @@ +var d3chart, line, bar; +d3chart = require('kraken/chart/type/d3/d3-chart-element'); +line = require('kraken/chart/type/d3/d3-line-element'); +bar = require('kraken/chart/type/d3/d3-bar-element'); +__import(__import(__import(exports, line), bar), d3chart); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/type/d3/index.mod.js b/lib/chart/type/d3/index.mod.js new file mode 100644 index 0000000..9c2a284 --- /dev/null +++ b/lib/chart/type/d3/index.mod.js @@ -0,0 +1,14 @@ +require.define('/node_modules/kraken/chart/type/d3/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var d3chart, line, bar; +d3chart = require('kraken/chart/type/d3/d3-chart-element'); +line = require('kraken/chart/type/d3/d3-line-element'); +bar = require('kraken/chart/type/d3/d3-bar-element'); +__import(__import(__import(exports, line), bar), d3chart); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/type/dygraphs.js b/lib/chart/type/dygraphs.js new file mode 100644 index 0000000..59865da --- /dev/null +++ b/lib/chart/type/dygraphs.js @@ -0,0 +1,127 @@ +var ChartType, DygraphsChartType, _; +_ = require('kraken/util/underscore'); +ChartType = require('kraken/chart/chart-type').ChartType; +exports.DygraphsChartType = DygraphsChartType = (function(superclass){ + DygraphsChartType.displayName = 'DygraphsChartType'; + var prototype = __extend(DygraphsChartType, superclass).prototype, constructor = DygraphsChartType; + prototype.__bind__ = ['dygNumberFormatter', 'dygNumberFormatterHTML']; + prototype.SPEC_URL = '/schema/dygraph.json'; + prototype.typeName = 'dygraphs'; + ChartType.register(DygraphsChartType); + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport', + legend: '.graph-legend' + }; + function DygraphsChartType(){ + superclass.apply(this, arguments); + } + prototype.makeAxisFormatter = function(fmttr){ + return function(n, granularity, opts, g){ + return fmttr(n, opts, g); + }; + }; + prototype.dygAxisDateFormatter = function(n, granularity, opts, g){ + return moment(n).format('MM/YYYY'); + }; + prototype.dygDateFormatter = function(n, opts, g){ + return moment(n).format('DD MMM YYYY'); + }; + prototype.dygNumberFormatter = function(n, opts, g){ + var that, digits, whole, fraction, suffix, _ref; + digits = (that = typeof opts('digitsAfterDecimal') === 'number') ? that : 2; + _ref = this.numberFormatter(n, digits), whole = _ref.whole, fraction = _ref.fraction, suffix = _ref.suffix; + return whole + "" + fraction + suffix; + }; + prototype.dygNumberFormatterHTML = function(n, opts, g){ + var that, digits, whole, fraction, suffix, _ref; + digits = (that = typeof opts('digitsAfterDecimal') === 'number') ? that : 2; + _ref = this.numberFormatter(n, digits), whole = _ref.whole, fraction = _ref.fraction, suffix = _ref.suffix; + return "" + whole + "" + fraction + "" + suffix + ""; + }; + /** + * Determines chart viewport size. + * @return { width, height } + */ + prototype.determineSize = function(){ + var width, modelW, height, modelH, viewport, legend, vpWidth, legendW; + modelW = width = this.model.get('width'); + modelH = height = this.model.get('height'); + if (!(this.view.ready && width && height)) { + return { + width: width, + height: height + }; + } + viewport = this.getElementsForRole('viewport'); + legend = this.getElementsForRole('legend'); + if (width === 'auto') { + delete viewport.prop('style').width; + vpWidth = viewport.innerWidth() || 300; + legendW = legend.outerWidth() || 228; + width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW); + } + width == null && (width = modelW); + if (height === 'auto') { + delete viewport.prop('style').height; + height = viewport.innerHeight() || 320; + } + height == null && (height = modelH); + return { + width: width, + height: height + }; + }; + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * @returns {Object} The derived chart options. + */ + prototype.transform = function(){ + var dataset, options; + dataset = this.model.dataset; + options = __import(this.view.chartOptions(), this.determineSize()); + return __import(options, { + colors: dataset.getColors(), + labels: dataset.getLabels(), + labelsDiv: this.getElementsForRole('legend')[0], + valueFormatter: this.dygNumberFormatterHTML, + axes: { + x: { + axisLabelFormatter: this.dygAxisDateFormatter, + valueFormatter: this.dygDateFormatter + }, + y: { + axisLabelFormatter: this.makeAxisFormatter(this.dygNumberFormatter), + valueFormatter: this.dygNumberFormatterHTML + } + } + }); + }; + /** + * @returns {Dygraph} The Dygraph chart object. + */ + prototype.renderChart = function(data, viewport, options, lastChart){ + this.resizeViewport(); + if (lastChart != null) { + lastChart.destroy(); + } + return new Dygraph(viewport[0], data, options); + }; + return DygraphsChartType; +}(ChartType)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/chart/type/dygraphs.mod.js b/lib/chart/type/dygraphs.mod.js new file mode 100644 index 0000000..68e7d1f --- /dev/null +++ b/lib/chart/type/dygraphs.mod.js @@ -0,0 +1,131 @@ +require.define('/node_modules/kraken/chart/type/dygraphs.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var ChartType, DygraphsChartType, _; +_ = require('kraken/util/underscore'); +ChartType = require('kraken/chart/chart-type').ChartType; +exports.DygraphsChartType = DygraphsChartType = (function(superclass){ + DygraphsChartType.displayName = 'DygraphsChartType'; + var prototype = __extend(DygraphsChartType, superclass).prototype, constructor = DygraphsChartType; + prototype.__bind__ = ['dygNumberFormatter', 'dygNumberFormatterHTML']; + prototype.SPEC_URL = '/schema/dygraph.json'; + prototype.typeName = 'dygraphs'; + ChartType.register(DygraphsChartType); + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport', + legend: '.graph-legend' + }; + function DygraphsChartType(){ + superclass.apply(this, arguments); + } + prototype.makeAxisFormatter = function(fmttr){ + return function(n, granularity, opts, g){ + return fmttr(n, opts, g); + }; + }; + prototype.dygAxisDateFormatter = function(n, granularity, opts, g){ + return moment(n).format('MM/YYYY'); + }; + prototype.dygDateFormatter = function(n, opts, g){ + return moment(n).format('DD MMM YYYY'); + }; + prototype.dygNumberFormatter = function(n, opts, g){ + var that, digits, whole, fraction, suffix, _ref; + digits = (that = typeof opts('digitsAfterDecimal') === 'number') ? that : 2; + _ref = this.numberFormatter(n, digits), whole = _ref.whole, fraction = _ref.fraction, suffix = _ref.suffix; + return whole + "" + fraction + suffix; + }; + prototype.dygNumberFormatterHTML = function(n, opts, g){ + var that, digits, whole, fraction, suffix, _ref; + digits = (that = typeof opts('digitsAfterDecimal') === 'number') ? that : 2; + _ref = this.numberFormatter(n, digits), whole = _ref.whole, fraction = _ref.fraction, suffix = _ref.suffix; + return "" + whole + "" + fraction + "" + suffix + ""; + }; + /** + * Determines chart viewport size. + * @return { width, height } + */ + prototype.determineSize = function(){ + var width, modelW, height, modelH, viewport, legend, vpWidth, legendW; + modelW = width = this.model.get('width'); + modelH = height = this.model.get('height'); + if (!(this.view.ready && width && height)) { + return { + width: width, + height: height + }; + } + viewport = this.getElementsForRole('viewport'); + legend = this.getElementsForRole('legend'); + if (width === 'auto') { + delete viewport.prop('style').width; + vpWidth = viewport.innerWidth() || 300; + legendW = legend.outerWidth() || 228; + width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW); + } + width == null && (width = modelW); + if (height === 'auto') { + delete viewport.prop('style').height; + height = viewport.innerHeight() || 320; + } + height == null && (height = modelH); + return { + width: width, + height: height + }; + }; + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * @returns {Object} The derived chart options. + */ + prototype.transform = function(){ + var dataset, options; + dataset = this.model.dataset; + options = __import(this.view.chartOptions(), this.determineSize()); + return __import(options, { + colors: dataset.getColors(), + labels: dataset.getLabels(), + labelsDiv: this.getElementsForRole('legend')[0], + valueFormatter: this.dygNumberFormatterHTML, + axes: { + x: { + axisLabelFormatter: this.dygAxisDateFormatter, + valueFormatter: this.dygDateFormatter + }, + y: { + axisLabelFormatter: this.makeAxisFormatter(this.dygNumberFormatter), + valueFormatter: this.dygNumberFormatterHTML + } + } + }); + }; + /** + * @returns {Dygraph} The Dygraph chart object. + */ + prototype.renderChart = function(data, viewport, options, lastChart){ + this.resizeViewport(); + if (lastChart != null) { + lastChart.destroy(); + } + return new Dygraph(viewport[0], data, options); + }; + return DygraphsChartType; +}(ChartType)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/chart/type/index.js b/lib/chart/type/index.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/chart/type/index.mod.js b/lib/chart/type/index.mod.js new file mode 100644 index 0000000..c1940bf --- /dev/null +++ b/lib/chart/type/index.mod.js @@ -0,0 +1,5 @@ +require.define('/node_modules/kraken/chart/type/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + + + +}); diff --git a/lib/dashboard/dashboard-model.js b/lib/dashboard/dashboard-model.js new file mode 100644 index 0000000..8e26e25 --- /dev/null +++ b/lib/dashboard/dashboard-model.js @@ -0,0 +1,87 @@ +var op, BaseModel, Graph, GraphList, Dashboard, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseModel = require('kraken/base').BaseModel; +_ref = require('kraken/graph/graph-model'), Graph = _ref.Graph, GraphList = _ref.GraphList; +/** + * @class + */ +Dashboard = exports.Dashboard = BaseModel.extend({ + urlRoot: '/dashboards', + graphs: null, + constructor: (function(){ + function Dashboard(){ + this.graphs = new GraphList; + return BaseModel.apply(this, arguments); + } + return Dashboard; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + }, + defaults: function(){ + return { + name: null, + tabs: [{ + name: "Main", + graph_ids: [] + }] + }; + }, + load: function(){ + var _this = this; + this.once('fetch-success', function(){ + return _this.getGraphs(); + }).loadModel(); + return this; + } + /** + * Look up a tab. + * + * @param {String|Number} tab Tab name or index. + * @returns {Tab} Tab object. + */, + getTab: function(tab){ + var tabs; + tabs = this.get('tabs'); + if (typeof tab === 'number') { + return tabs[tab]; + } + return _.find(tabs, function(it){ + return it.name === tab; + }); + }, + show: function(cb, obj){ + console.log('[show]'); + console.log(obj); + return cb(null, obj); + }, + pushAsync: function(cb, arr){ + return function(err, elem){ + arr.push(elem); + return cb(null); + }; + }, + getGraphs: function(){ + var graph_ids, _this = this; + console.log('[getGraphs]\tentering'); + graph_ids = _(this.tabs).chain().values().map(function(tab_obj){ + return tab_obj.graph_ids; + }).flatten().value(); + Seq(graph_ids).parMap_(function(next, graph_id){ + return next(null, [graph_id]); + }).parEach_(function(next, graph_id_arr){ + return Graph.lookup(graph_id_arr[0], _this.pushAsync(next, graph_id_arr)); + }).parMap_(function(next, tuple){ + var id, graph; + id = tuple[0], graph = tuple[1]; + return graph.once('ready', function(){ + return next.ok(tuple); + }); + }).unflatten().seq_(function(next, graph_tuples){ + _this.graphs.reset(_.pluck(graph_tuples, 1)); + console.log('[setter]\tcalling ready'); + return _this.triggerReady(); + }); + return this; + } +}); \ No newline at end of file diff --git a/lib/dashboard/dashboard-model.mod.js b/lib/dashboard/dashboard-model.mod.js new file mode 100644 index 0000000..7e61eb7 --- /dev/null +++ b/lib/dashboard/dashboard-model.mod.js @@ -0,0 +1,91 @@ +require.define('/node_modules/kraken/dashboard/dashboard-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseModel, Graph, GraphList, Dashboard, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseModel = require('kraken/base').BaseModel; +_ref = require('kraken/graph/graph-model'), Graph = _ref.Graph, GraphList = _ref.GraphList; +/** + * @class + */ +Dashboard = exports.Dashboard = BaseModel.extend({ + urlRoot: '/dashboards', + graphs: null, + constructor: (function(){ + function Dashboard(){ + this.graphs = new GraphList; + return BaseModel.apply(this, arguments); + } + return Dashboard; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + }, + defaults: function(){ + return { + name: null, + tabs: [{ + name: "Main", + graph_ids: [] + }] + }; + }, + load: function(){ + var _this = this; + this.once('fetch-success', function(){ + return _this.getGraphs(); + }).loadModel(); + return this; + } + /** + * Look up a tab. + * + * @param {String|Number} tab Tab name or index. + * @returns {Tab} Tab object. + */, + getTab: function(tab){ + var tabs; + tabs = this.get('tabs'); + if (typeof tab === 'number') { + return tabs[tab]; + } + return _.find(tabs, function(it){ + return it.name === tab; + }); + }, + show: function(cb, obj){ + console.log('[show]'); + console.log(obj); + return cb(null, obj); + }, + pushAsync: function(cb, arr){ + return function(err, elem){ + arr.push(elem); + return cb(null); + }; + }, + getGraphs: function(){ + var graph_ids, _this = this; + console.log('[getGraphs]\tentering'); + graph_ids = _(this.tabs).chain().values().map(function(tab_obj){ + return tab_obj.graph_ids; + }).flatten().value(); + Seq(graph_ids).parMap_(function(next, graph_id){ + return next(null, [graph_id]); + }).parEach_(function(next, graph_id_arr){ + return Graph.lookup(graph_id_arr[0], _this.pushAsync(next, graph_id_arr)); + }).parMap_(function(next, tuple){ + var id, graph; + id = tuple[0], graph = tuple[1]; + return graph.once('ready', function(){ + return next.ok(tuple); + }); + }).unflatten().seq_(function(next, graph_tuples){ + _this.graphs.reset(_.pluck(graph_tuples, 1)); + console.log('[setter]\tcalling ready'); + return _this.triggerReady(); + }); + return this; + } +}); + +}); diff --git a/lib/dashboard/dashboard-view.js b/lib/dashboard/dashboard-view.js new file mode 100644 index 0000000..37b4d79 --- /dev/null +++ b/lib/dashboard/dashboard-view.js @@ -0,0 +1,148 @@ +var Seq, op, BaseModel, BaseView, Graph, GraphList, GraphDisplayView, Dashboard, DashboardView, DashboardTabView, _ref, _; +Seq = require('seq'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseView = _ref.BaseView; +_ref = require('kraken/graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView; +Dashboard = require('kraken/dashboard/dashboard-model').Dashboard; +/** + * @class + */ +DashboardView = exports.DashboardView = BaseView.extend({ + __bind__: ['addTab'], + tagName: 'section', + className: 'dashboard', + template: require('kraken/template/dashboard/dashboard'), + events: { + 'click .graphs.tabbable .nav a': 'onTabClick', + 'shown .graphs.tabbable .nav a': 'render' + }, + graphs: null, + ready: false, + constructor: (function(){ + function DashboardView(options){ + options == null && (options = {}); + this.graphs = new GraphList; + return BaseView.apply(this, arguments); + } + return DashboardView; + }()), + initialize: function(){ + this.model || (this.model = new Dashboard); + DashboardView.__super__.initialize.apply(this, arguments); + return this.model.once('ready', this.load, this).load(); + }, + load: function(){ + var _this = this; + console.log(this + ".load! Model ready!", this.model); + return Seq(this.model.get('tabs')).seqEach_(this.addTab).seq(function(){ + console.log(_this + ".load! Done adding tabs!"); + return _this.triggerReady(); + }); + }, + addTab: function(nextTab, tab){ + var tabModel, tabView, tabId, graphs, _this = this; + tabModel = new BaseModel(tab); + tabView = this.addSubview(new DashboardTabView({ + model: tabModel + })); + tabId = tabView.getTabId(); + this.$("nav > ul.nav").append("
  • " + tab.name + "
  • "); + graphs = _(tab.graph_ids).map(function(graph_id){ + return _this.model.graphs.get(graph_id); + }); + Seq(graphs).parMap_(function(next, graph){ + _this.graphs.add(graph); + return next(null, new GraphDisplayView({ + model: graph + })); + }).parMap_(function(next, graphView){ + if (graphView.isAttached) { + return next.ok(); + } + tabView.addSubview(graphView); + return next.ok(); + }).seq(function(){ + console.log(_this + ".addTab: All graphs added!"); + return nextTab.ok(); + }); + return this; + }, + onTabShown: function(e){ + return this.render(); + }, + onTabClick: function(evt){ + return evt.preventDefault(); + } + /** + * Scroll to the specified graph. + * + * @param {String|Number|Graph} graph The Graph to scroll to; can be specified as a + * Graph id, an index into the Graphs list, or a Graph object. + * @returns {this} + */, + scrollToGraph: function(graph){ + var view; + if (typeof graph === 'string') { + graph = this.graphs.get(graph); + } else if (typeof graph === 'number') { + graph = this.graphs.at(graph); + } + if (!(graph instanceof Graph)) { + console.error(this + ".scrollToGraph() Unknown graph " + graph + "!"); + return this; + } + if (!(view = _.find(this.subviews, function(it){ + return it.model === graph; + }))) { + return this; + } + if (view.$el.is(':visible')) { + $('body').scrollTop(view.$el.offset().top); + } + return this; + }, + findClosestGraph: function(scroll){ + var views; + scroll || (scroll = $('body').scrollTop()); + views = this.subviews.filter(function(it){ + return it.$el.is(':visible'); + }).map(function(it){ + return [it.$el.offset().top, it]; + }).filter(function(it){ + return it[0] >= scroll; + }).sort(function(a, b){ + return op.cmp(a[0], b[0]); + }); + if (views.length) { + return views[0][1]; + } + } +}); +/** + * @class + * @extends BaseView + */ +DashboardTabView = exports.DashboardTabView = BaseView.extend({ + __bind__: [], + className: 'tab-pane', + tag: 'div', + template: require('kraken/template/dashboard/dashboard-tab'), + constructor: (function(){ + function DashboardTabView(){ + return BaseView.apply(this, arguments); + } + return DashboardTabView; + }()), + initialize: function(){ + return BaseView.prototype.initialize.apply(this, arguments); + }, + getTabId: function(){ + return _.underscored(this.model.get('name')).toLowerCase() + '-graphs-tab'; + }, + toTemplateLocals: function(){ + var json, tab_name; + json = DashboardTabView.__super__.toTemplateLocals.apply(this, arguments); + tab_name = _.underscored(this.model.get('name')).toLowerCase(); + return json.tab_cls = tab_name + "-graphs-pane", json.tab_id = tab_name + "-graphs-tab", json; + } +}); \ No newline at end of file diff --git a/lib/dashboard/dashboard-view.mod.js b/lib/dashboard/dashboard-view.mod.js new file mode 100644 index 0000000..cc0228a --- /dev/null +++ b/lib/dashboard/dashboard-view.mod.js @@ -0,0 +1,152 @@ +require.define('/node_modules/kraken/dashboard/dashboard-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, op, BaseModel, BaseView, Graph, GraphList, GraphDisplayView, Dashboard, DashboardView, DashboardTabView, _ref, _; +Seq = require('seq'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseView = _ref.BaseView; +_ref = require('kraken/graph'), Graph = _ref.Graph, GraphList = _ref.GraphList, GraphDisplayView = _ref.GraphDisplayView; +Dashboard = require('kraken/dashboard/dashboard-model').Dashboard; +/** + * @class + */ +DashboardView = exports.DashboardView = BaseView.extend({ + __bind__: ['addTab'], + tagName: 'section', + className: 'dashboard', + template: require('kraken/template/dashboard/dashboard'), + events: { + 'click .graphs.tabbable .nav a': 'onTabClick', + 'shown .graphs.tabbable .nav a': 'render' + }, + graphs: null, + ready: false, + constructor: (function(){ + function DashboardView(options){ + options == null && (options = {}); + this.graphs = new GraphList; + return BaseView.apply(this, arguments); + } + return DashboardView; + }()), + initialize: function(){ + this.model || (this.model = new Dashboard); + DashboardView.__super__.initialize.apply(this, arguments); + return this.model.once('ready', this.load, this).load(); + }, + load: function(){ + var _this = this; + console.log(this + ".load! Model ready!", this.model); + return Seq(this.model.get('tabs')).seqEach_(this.addTab).seq(function(){ + console.log(_this + ".load! Done adding tabs!"); + return _this.triggerReady(); + }); + }, + addTab: function(nextTab, tab){ + var tabModel, tabView, tabId, graphs, _this = this; + tabModel = new BaseModel(tab); + tabView = this.addSubview(new DashboardTabView({ + model: tabModel + })); + tabId = tabView.getTabId(); + this.$("nav > ul.nav").append("
  • " + tab.name + "
  • "); + graphs = _(tab.graph_ids).map(function(graph_id){ + return _this.model.graphs.get(graph_id); + }); + Seq(graphs).parMap_(function(next, graph){ + _this.graphs.add(graph); + return next(null, new GraphDisplayView({ + model: graph + })); + }).parMap_(function(next, graphView){ + if (graphView.isAttached) { + return next.ok(); + } + tabView.addSubview(graphView); + return next.ok(); + }).seq(function(){ + console.log(_this + ".addTab: All graphs added!"); + return nextTab.ok(); + }); + return this; + }, + onTabShown: function(e){ + return this.render(); + }, + onTabClick: function(evt){ + return evt.preventDefault(); + } + /** + * Scroll to the specified graph. + * + * @param {String|Number|Graph} graph The Graph to scroll to; can be specified as a + * Graph id, an index into the Graphs list, or a Graph object. + * @returns {this} + */, + scrollToGraph: function(graph){ + var view; + if (typeof graph === 'string') { + graph = this.graphs.get(graph); + } else if (typeof graph === 'number') { + graph = this.graphs.at(graph); + } + if (!(graph instanceof Graph)) { + console.error(this + ".scrollToGraph() Unknown graph " + graph + "!"); + return this; + } + if (!(view = _.find(this.subviews, function(it){ + return it.model === graph; + }))) { + return this; + } + if (view.$el.is(':visible')) { + $('body').scrollTop(view.$el.offset().top); + } + return this; + }, + findClosestGraph: function(scroll){ + var views; + scroll || (scroll = $('body').scrollTop()); + views = this.subviews.filter(function(it){ + return it.$el.is(':visible'); + }).map(function(it){ + return [it.$el.offset().top, it]; + }).filter(function(it){ + return it[0] >= scroll; + }).sort(function(a, b){ + return op.cmp(a[0], b[0]); + }); + if (views.length) { + return views[0][1]; + } + } +}); +/** + * @class + * @extends BaseView + */ +DashboardTabView = exports.DashboardTabView = BaseView.extend({ + __bind__: [], + className: 'tab-pane', + tag: 'div', + template: require('kraken/template/dashboard/dashboard-tab'), + constructor: (function(){ + function DashboardTabView(){ + return BaseView.apply(this, arguments); + } + return DashboardTabView; + }()), + initialize: function(){ + return BaseView.prototype.initialize.apply(this, arguments); + }, + getTabId: function(){ + return _.underscored(this.model.get('name')).toLowerCase() + '-graphs-tab'; + }, + toTemplateLocals: function(){ + var json, tab_name; + json = DashboardTabView.__super__.toTemplateLocals.apply(this, arguments); + tab_name = _.underscored(this.model.get('name')).toLowerCase(); + return json.tab_cls = tab_name + "-graphs-pane", json.tab_id = tab_name + "-graphs-tab", json; + } +}); + +}); diff --git a/lib/dashboard/index.js b/lib/dashboard/index.js new file mode 100644 index 0000000..078918a --- /dev/null +++ b/lib/dashboard/index.js @@ -0,0 +1,9 @@ +var models, views; +models = require('kraken/dashboard/dashboard-model'); +views = require('kraken/dashboard/dashboard-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/dashboard/index.mod.js b/lib/dashboard/index.mod.js new file mode 100644 index 0000000..5471d02 --- /dev/null +++ b/lib/dashboard/index.mod.js @@ -0,0 +1,13 @@ +require.define('/node_modules/kraken/dashboard/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var models, views; +models = require('kraken/dashboard/dashboard-model'); +views = require('kraken/dashboard/dashboard-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/data/data-view.js b/lib/data/data-view.js new file mode 100644 index 0000000..08e1688 --- /dev/null +++ b/lib/data/data-view.js @@ -0,0 +1,142 @@ +var Seq, op, BaseView, ViewList, DataSetView, MetricEditView, DataSource, DataView, _ref, _; +Seq = require('seq'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseView = _ref.BaseView, ViewList = _ref.ViewList; +DataSetView = require('kraken/data/dataset-view').DataSetView; +MetricEditView = require('kraken/data/metric-edit-view').MetricEditView; +DataSource = require('kraken/data/datasource-model').DataSource; +/** + * @class DataSet selection and customization UI (root of the `data` tab). + */ +DataView = exports.DataView = BaseView.extend({ + __bind__: ['onMetricsChanged'], + tagName: 'section', + className: 'data-ui', + template: require('kraken/template/data/data'), + datasources: null + /** + * @constructor + */, + constructor: (function(){ + function DataView(){ + return BaseView.apply(this, arguments); + } + return DataView; + }()), + initialize: function(){ + this.graph_id = this.options.graph_id; + BaseView.prototype.initialize.apply(this, arguments); + this.metric_views = new ViewList; + this.datasources = DataSource.getAllSources(); + this.model.metrics.on('add', this.addMetric, this).on('remove', this.removeMetric, this); + return this.model.once('ready', this.onReady, this); + }, + onReady: function(){ + var dataset; + dataset = this.model; + this.model.metrics.each(this.addMetric, this); + this.dataset_view = new DataSetView({ + model: this.model, + graph_id: this.graph_id, + dataset: dataset, + datasources: this.datasources + }); + this.addSubview(this.dataset_view).on('add-metric', this.onMetricsChanged, this).on('remove-metric', this.onMetricsChanged, this).on('select-metric', this.selectMetric, this); + this.render(); + this.triggerReady(); + return this; + } + /** + * Transform the `columns` field to ensure an Array of {label, type} objects. + */, + canonicalizeDataSource: function(ds){ + var cols; + ds.shortName || (ds.shortName = ds.name); + ds.title || (ds.title = ds.name); + ds.subtitle || (ds.subtitle = ''); + cols = ds.columns; + if (_.isArray(cols)) { + ds.metrics = _.map(cols, function(col, idx){ + var label, type; + if (_.isArray(col)) { + label = col[0], type = col[1]; + return { + idx: idx, + label: label, + type: type || 'int' + }; + } else { + return col; + } + }); + } else { + ds.metrics = _.map(cols.labels, function(label, idx){ + return { + idx: idx, + label: label, + type: cols.types[idx] || 'int' + }; + }); + } + return ds; + }, + toTemplateLocals: function(){ + var attrs; + attrs = _.clone(this.model.attributes); + return __import({ + graph_id: this.graph_id, + datasources: this.datasources + }, attrs); + }, + addMetric: function(metric){ + var view; + if (this.metric_views.findByModel(metric)) { + return metric; + } + view = new MetricEditView({ + model: metric, + graph_id: this.graph_id, + dataset: this.model, + datasources: this.datasources + }).on('metric-update', this.onUpdateMetric, this).on('metric-change', this.onUpdateMetric, this); + this.metric_views.push(this.addSubview(view)); + this.renderSubviews(); + return metric; + }, + removeMetric: function(metric){ + var view; + if (!(view = this.metric_views.findByModel(metric))) { + return; + } + this.metric_views.remove(view); + this.removeSubview(view); + return metric; + }, + selectMetric: function(metric){ + var _ref; + this.metric_views.invoke('hide'); + this.metric_edit_view = this.metric_views.findByModel(metric); + if ((_ref = this.metric_edit_view) != null) { + _ref.show(); + } + return _.delay(this.onMetricsChanged, 10); + }, + onMetricsChanged: function(){ + var oldMinHeight, newMinHeight, _ref; + if (!this.dataset_view) { + return; + } + oldMinHeight = parseInt(this.$el.css('min-height')); + newMinHeight = Math.max(this.dataset_view.$el.height(), (_ref = this.metric_edit_view) != null ? _ref.$el.height() : void 8); + return this.$el.css('min-height', newMinHeight); + }, + onUpdateMetric: function(){ + this.trigger('metric-change', this.model, this); + return this.render(); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/data/data-view.mod.js b/lib/data/data-view.mod.js new file mode 100644 index 0000000..73e06f5 --- /dev/null +++ b/lib/data/data-view.mod.js @@ -0,0 +1,146 @@ +require.define('/node_modules/kraken/data/data-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, op, BaseView, ViewList, DataSetView, MetricEditView, DataSource, DataView, _ref, _; +Seq = require('seq'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseView = _ref.BaseView, ViewList = _ref.ViewList; +DataSetView = require('kraken/data/dataset-view').DataSetView; +MetricEditView = require('kraken/data/metric-edit-view').MetricEditView; +DataSource = require('kraken/data/datasource-model').DataSource; +/** + * @class DataSet selection and customization UI (root of the `data` tab). + */ +DataView = exports.DataView = BaseView.extend({ + __bind__: ['onMetricsChanged'], + tagName: 'section', + className: 'data-ui', + template: require('kraken/template/data/data'), + datasources: null + /** + * @constructor + */, + constructor: (function(){ + function DataView(){ + return BaseView.apply(this, arguments); + } + return DataView; + }()), + initialize: function(){ + this.graph_id = this.options.graph_id; + BaseView.prototype.initialize.apply(this, arguments); + this.metric_views = new ViewList; + this.datasources = DataSource.getAllSources(); + this.model.metrics.on('add', this.addMetric, this).on('remove', this.removeMetric, this); + return this.model.once('ready', this.onReady, this); + }, + onReady: function(){ + var dataset; + dataset = this.model; + this.model.metrics.each(this.addMetric, this); + this.dataset_view = new DataSetView({ + model: this.model, + graph_id: this.graph_id, + dataset: dataset, + datasources: this.datasources + }); + this.addSubview(this.dataset_view).on('add-metric', this.onMetricsChanged, this).on('remove-metric', this.onMetricsChanged, this).on('select-metric', this.selectMetric, this); + this.render(); + this.triggerReady(); + return this; + } + /** + * Transform the `columns` field to ensure an Array of {label, type} objects. + */, + canonicalizeDataSource: function(ds){ + var cols; + ds.shortName || (ds.shortName = ds.name); + ds.title || (ds.title = ds.name); + ds.subtitle || (ds.subtitle = ''); + cols = ds.columns; + if (_.isArray(cols)) { + ds.metrics = _.map(cols, function(col, idx){ + var label, type; + if (_.isArray(col)) { + label = col[0], type = col[1]; + return { + idx: idx, + label: label, + type: type || 'int' + }; + } else { + return col; + } + }); + } else { + ds.metrics = _.map(cols.labels, function(label, idx){ + return { + idx: idx, + label: label, + type: cols.types[idx] || 'int' + }; + }); + } + return ds; + }, + toTemplateLocals: function(){ + var attrs; + attrs = _.clone(this.model.attributes); + return __import({ + graph_id: this.graph_id, + datasources: this.datasources + }, attrs); + }, + addMetric: function(metric){ + var view; + if (this.metric_views.findByModel(metric)) { + return metric; + } + view = new MetricEditView({ + model: metric, + graph_id: this.graph_id, + dataset: this.model, + datasources: this.datasources + }).on('metric-update', this.onUpdateMetric, this).on('metric-change', this.onUpdateMetric, this); + this.metric_views.push(this.addSubview(view)); + this.renderSubviews(); + return metric; + }, + removeMetric: function(metric){ + var view; + if (!(view = this.metric_views.findByModel(metric))) { + return; + } + this.metric_views.remove(view); + this.removeSubview(view); + return metric; + }, + selectMetric: function(metric){ + var _ref; + this.metric_views.invoke('hide'); + this.metric_edit_view = this.metric_views.findByModel(metric); + if ((_ref = this.metric_edit_view) != null) { + _ref.show(); + } + return _.delay(this.onMetricsChanged, 10); + }, + onMetricsChanged: function(){ + var oldMinHeight, newMinHeight, _ref; + if (!this.dataset_view) { + return; + } + oldMinHeight = parseInt(this.$el.css('min-height')); + newMinHeight = Math.max(this.dataset_view.$el.height(), (_ref = this.metric_edit_view) != null ? _ref.$el.height() : void 8); + return this.$el.css('min-height', newMinHeight); + }, + onUpdateMetric: function(){ + this.trigger('metric-change', this.model, this); + return this.render(); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/data/dataset-model.js b/lib/data/dataset-model.js new file mode 100644 index 0000000..2f75baa --- /dev/null +++ b/lib/data/dataset-model.js @@ -0,0 +1,184 @@ +var Seq, ColorBrewer, op, BaseModel, BaseList, Metric, MetricList, DataSource, DataSourceList, DataSet, _ref, _; +Seq = require('seq'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +_ref = require('kraken/data/metric-model'), Metric = _ref.Metric, MetricList = _ref.MetricList; +_ref = require('kraken/data/datasource-model'), DataSource = _ref.DataSource, DataSourceList = _ref.DataSourceList; +/** + * @class + */ +DataSet = exports.DataSet = BaseModel.extend({ + urlRoot: '/datasets' + /** + * @type DataSourceList + */, + sources: null + /** + * @type MetricList + */, + metrics: null, + defaults: function(){ + return { + palette: null, + lines: [], + metrics: [] + }; + }, + constructor: (function(){ + function DataSet(attributes, opts){ + attributes == null && (attributes = {}); + this.metrics = new MetricList(attributes.metrics); + return BaseModel.call(this, attributes, opts); + } + return DataSet; + }()), + initialize: function(){ + BaseModel.prototype.initialize.apply(this, arguments); + this.set('metrics', this.metrics, { + silent: true + }); + return this.on('change:metrics', this.onMetricChange, this); + }, + load: function(opts){ + var _this = this; + opts == null && (opts = {}); + if (opts.force) { + this.resetReady(); + } + if (this.loading || this.ready) { + return this; + } + if (!this.metrics.length) { + return this.triggerReady(); + } + this.wait(); + this.loading = true; + this.trigger('load', this); + Seq(this.metrics.models).parEach_(function(next, metric){ + return metric.once('ready', next.ok).load(); + }).seq(function(){ + _this.loading = false; + _this.unwait(); + return _this.triggerReady(); + }); + return this; + } + /** + * Override to handle the case where one of our rich sub-objects + * (basically `metrics`) is set as a result of the `fetch()` call by the + * Graph object. To prevent it from blowing away the `MetricList`, we + * perform a `reset()` here. But that won't trigger a `change:metrics` event, + * so we do a little dance to set it twice, as object identity would otherwise + * cause it to think nothing has changed. + */, + set: function(key, value, opts){ + var values, _ref; + if (_.isObject(key) && key != null) { + _ref = [key, value], values = _ref[0], opts = _ref[1]; + } else { + values = (_ref = {}, _ref[key + ""] = value, _ref); + } + opts || (opts = {}); + for (key in values) { + value = values[key]; + if (!(key === 'metrics' && _.isArray(value))) { + continue; + } + this.metrics.reset(value); + delete values[key]; + if (!opts.silent) { + DataSet.__super__.set.call(this, 'metrics', value, { + silent: true + }); + DataSet.__super__.set.call(this, 'metrics', this.metrics, opts); + } + } + return DataSet.__super__.set.call(this, values, opts); + }, + toJSON: function(){ + var json; + json = DataSet.__super__.toJSON.apply(this, arguments); + delete json.id; + return json; + } + /* * * * TimeSeriesData interface * * * {{{ */ + /** + * @returns {Array} The reified dataset, materialized to a list of rows including timestamps. + */, + getData: function(){ + var columns; + if (!this.ready) { + return []; + } + columns = this.getColumns(); + if (columns != null && columns.length) { + return _.zip.apply(_, columns); + } else { + return []; + } + } + /** + * @returns {Array} List of all columns (including date column). + */, + getColumns: function(){ + if (!this.ready) { + return []; + } + return _.compact([this.getDateColumn()].concat(this.getDataColumns())); + } + /** + * @returns {Array} The date column. + */, + getDateColumn: function(){ + var dates, maxLen; + if (!this.ready) { + return []; + } + dates = this.metrics.onlyOk().invoke('getDateColumn'); + maxLen = _.max(_.pluck(dates, 'length')); + return _.find(dates, function(it){ + return it.length === maxLen; + }); + } + /** + * @returns {Array} List of all columns except the date column. + */, + getDataColumns: function(){ + if (!this.ready) { + return []; + } + return this.metrics.onlyOk().invoke('getData'); + } + /** + * @returns {Array} List of column labels. + */, + getLabels: function(){ + if (!this.ready) { + return []; + } + return ['Date'].concat(this.metrics.onlyOk().invoke('getLabel')); + }, + getColors: function(){ + if (!this.ready) { + return []; + } + return this.metrics.onlyOk().invoke('getColor'); + }, + newMetric: function(){ + var index, m, _this = this; + index = this.metrics.length; + this.metrics.add(m = new Metric({ + index: index, + color: ColorBrewer.Spectral[11][index] + })); + m.on('ready', function(){ + return _this.trigger('metric-data-loaded', _this, m); + }); + return m; + }, + onMetricChange: function(){ + this.resetReady(); + return this.load(); + } +}); \ No newline at end of file diff --git a/lib/data/dataset-model.mod.js b/lib/data/dataset-model.mod.js new file mode 100644 index 0000000..f3c47b1 --- /dev/null +++ b/lib/data/dataset-model.mod.js @@ -0,0 +1,188 @@ +require.define('/node_modules/kraken/data/dataset-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, ColorBrewer, op, BaseModel, BaseList, Metric, MetricList, DataSource, DataSourceList, DataSet, _ref, _; +Seq = require('seq'); +ColorBrewer = require('colorbrewer'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +_ref = require('kraken/data/metric-model'), Metric = _ref.Metric, MetricList = _ref.MetricList; +_ref = require('kraken/data/datasource-model'), DataSource = _ref.DataSource, DataSourceList = _ref.DataSourceList; +/** + * @class + */ +DataSet = exports.DataSet = BaseModel.extend({ + urlRoot: '/datasets' + /** + * @type DataSourceList + */, + sources: null + /** + * @type MetricList + */, + metrics: null, + defaults: function(){ + return { + palette: null, + lines: [], + metrics: [] + }; + }, + constructor: (function(){ + function DataSet(attributes, opts){ + attributes == null && (attributes = {}); + this.metrics = new MetricList(attributes.metrics); + return BaseModel.call(this, attributes, opts); + } + return DataSet; + }()), + initialize: function(){ + BaseModel.prototype.initialize.apply(this, arguments); + this.set('metrics', this.metrics, { + silent: true + }); + return this.on('change:metrics', this.onMetricChange, this); + }, + load: function(opts){ + var _this = this; + opts == null && (opts = {}); + if (opts.force) { + this.resetReady(); + } + if (this.loading || this.ready) { + return this; + } + if (!this.metrics.length) { + return this.triggerReady(); + } + this.wait(); + this.loading = true; + this.trigger('load', this); + Seq(this.metrics.models).parEach_(function(next, metric){ + return metric.once('ready', next.ok).load(); + }).seq(function(){ + _this.loading = false; + _this.unwait(); + return _this.triggerReady(); + }); + return this; + } + /** + * Override to handle the case where one of our rich sub-objects + * (basically `metrics`) is set as a result of the `fetch()` call by the + * Graph object. To prevent it from blowing away the `MetricList`, we + * perform a `reset()` here. But that won't trigger a `change:metrics` event, + * so we do a little dance to set it twice, as object identity would otherwise + * cause it to think nothing has changed. + */, + set: function(key, value, opts){ + var values, _ref; + if (_.isObject(key) && key != null) { + _ref = [key, value], values = _ref[0], opts = _ref[1]; + } else { + values = (_ref = {}, _ref[key + ""] = value, _ref); + } + opts || (opts = {}); + for (key in values) { + value = values[key]; + if (!(key === 'metrics' && _.isArray(value))) { + continue; + } + this.metrics.reset(value); + delete values[key]; + if (!opts.silent) { + DataSet.__super__.set.call(this, 'metrics', value, { + silent: true + }); + DataSet.__super__.set.call(this, 'metrics', this.metrics, opts); + } + } + return DataSet.__super__.set.call(this, values, opts); + }, + toJSON: function(){ + var json; + json = DataSet.__super__.toJSON.apply(this, arguments); + delete json.id; + return json; + } + /* * * * TimeSeriesData interface * * * {{{ */ + /** + * @returns {Array} The reified dataset, materialized to a list of rows including timestamps. + */, + getData: function(){ + var columns; + if (!this.ready) { + return []; + } + columns = this.getColumns(); + if (columns != null && columns.length) { + return _.zip.apply(_, columns); + } else { + return []; + } + } + /** + * @returns {Array} List of all columns (including date column). + */, + getColumns: function(){ + if (!this.ready) { + return []; + } + return _.compact([this.getDateColumn()].concat(this.getDataColumns())); + } + /** + * @returns {Array} The date column. + */, + getDateColumn: function(){ + var dates, maxLen; + if (!this.ready) { + return []; + } + dates = this.metrics.onlyOk().invoke('getDateColumn'); + maxLen = _.max(_.pluck(dates, 'length')); + return _.find(dates, function(it){ + return it.length === maxLen; + }); + } + /** + * @returns {Array} List of all columns except the date column. + */, + getDataColumns: function(){ + if (!this.ready) { + return []; + } + return this.metrics.onlyOk().invoke('getData'); + } + /** + * @returns {Array} List of column labels. + */, + getLabels: function(){ + if (!this.ready) { + return []; + } + return ['Date'].concat(this.metrics.onlyOk().invoke('getLabel')); + }, + getColors: function(){ + if (!this.ready) { + return []; + } + return this.metrics.onlyOk().invoke('getColor'); + }, + newMetric: function(){ + var index, m, _this = this; + index = this.metrics.length; + this.metrics.add(m = new Metric({ + index: index, + color: ColorBrewer.Spectral[11][index] + })); + m.on('ready', function(){ + return _this.trigger('metric-data-loaded', _this, m); + }); + return m; + }, + onMetricChange: function(){ + this.resetReady(); + return this.load(); + } +}); + +}); diff --git a/lib/data/dataset-view.js b/lib/data/dataset-view.js new file mode 100644 index 0000000..d1296fd --- /dev/null +++ b/lib/data/dataset-view.js @@ -0,0 +1,131 @@ +var op, BaseView, DataSetView, DataSetMetricView, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseView = require('kraken/base').BaseView; +/** + * @class + */ +DataSetView = exports.DataSetView = BaseView.extend({ + tagName: 'section', + className: 'dataset-ui dataset', + template: require('kraken/template/data/dataset'), + events: { + 'click .new-metric-button': 'onNewMetric', + 'click .delete-metric-button': 'onDeleteMetric', + 'click .metrics .dataset-metric': 'selectMetric' + }, + views_by_cid: {}, + active_view: null, + constructor: (function(){ + function DataSetView(){ + return BaseView.apply(this, arguments); + } + return DataSetView; + }()), + initialize: function(){ + var _ref; + _ref = this.options, this.graph_id = _ref.graph_id, this.datasources = _ref.datasources, this.dataset = _ref.dataset; + BaseView.prototype.initialize.apply(this, arguments); + this.views_by_cid = {}; + this.model.on('ready', this.addAllMetrics, this); + return this.model.metrics.on('add', this.addMetric, this).on('remove', this.removeMetric, this).on('change', this.onMetricChange, this).on('reset', this.addAllMetrics, this); + }, + addMetric: function(metric){ + var that, view; + if (that = this.views_by_cid[metric.cid]) { + this.removeSubview(that); + delete this.views_by_cid[metric.cid]; + } + view = this.addSubview(new DataSetMetricView({ + model: metric, + graph_id: this.graph_id + })); + this.views_by_cid[metric.cid] = view; + this.trigger('add-metric', metric, view, this); + this.render(); + return view; + }, + removeMetric: function(metric){ + var view; + if (metric instanceof jQuery.Event || metric instanceof Event) { + metric = this.getMetricForElement(metric.target); + } + if (!metric) { + return; + } + if (view = this.views_by_cid[metric.cid]) { + this.removeSubview(view); + delete this.views_by_cid[metric.cid]; + this.trigger('remove-metric', metric, view, this); + } + return view; + }, + addAllMetrics: function(){ + this.removeAllSubviews(); + this.model.metrics.each(this.addMetric, this); + return this; + }, + selectMetric: function(metric){ + var view; + if (metric instanceof jQuery.Event || metric instanceof Event) { + metric = this.getMetricForElement(metric.target); + } + if (!metric) { + return; + } + view = this.active_view = this.views_by_cid[metric.cid]; + this.$('.metrics .dataset-metric').removeClass('metric-active'); + view.$el.addClass('metric-active'); + view.$('.activity-arrow').css('font-size', 2 + view.$el.height()); + this.trigger('select-metric', metric, view, this); + return this; + }, + onMetricChange: function(metric){ + var view; + if (!(view = this.views_by_cid[metric != null ? metric.cid : void 8])) { + return; + } + return view.$('.activity-arrow:visible').css('font-size', 2 + view.$el.height()); + }, + onNewMetric: function(){ + this.model.newMetric(); + return false; + }, + onDeleteMetric: function(evt){ + var metric; + metric = this.getMetricForElement(evt.target); + this.model.metrics.remove(metric); + return false; + }, + getMetricForElement: function(el){ + return $(el).parents('.dataset-metric').eq(0).data('model'); + } +}); +/** + * @class + */ +DataSetMetricView = exports.DataSetMetricView = BaseView.extend({ + tagName: 'tr', + className: 'dataset-metric metric', + template: require('kraken/template/data/dataset-metric'), + constructor: (function(){ + function DataSetMetricView(){ + return BaseView.apply(this, arguments); + } + return DataSetMetricView; + }()), + initialize: function(){ + this.graph_id = this.options.graph_id; + BaseView.prototype.initialize.apply(this, arguments); + return this.on('update', this.onUpdate, this); + }, + toTemplateLocals: function(){ + var m, ts; + m = DataSetMetricView.__super__.toTemplateLocals.apply(this, arguments); + return m.graph_id = this.graph_id, m.label = this.model.getLabel(), m.viewClasses = _.compact([this.model.isOk() ? 'valid' : 'invalid', m.visible ? 'visible' : 'hidden', m.disabled ? 'disabled' : void 8]).map(function(it){ + 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 : '—', 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 index 0000000..b5aec94 --- /dev/null +++ b/lib/data/dataset-view.mod.js @@ -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 : '—', 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 index 0000000..8b72068 --- /dev/null +++ b/lib/data/datasource-model.js @@ -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 index 0000000..49b9f7e --- /dev/null +++ b/lib/data/datasource-model.mod.js @@ -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 index 0000000..0f2b4c7 --- /dev/null +++ b/lib/data/datasource-ui-view.js @@ -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 + ? '' + : 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 + ? '' + : ds.get('shortName'); + hasMetric = hasSource && this.model.get('source_col') != null; + locals.metric_summary = !hasMetric + ? '' + : ts.start + " — " + 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 index 0000000..3ec50e1 --- /dev/null +++ b/lib/data/datasource-view.js @@ -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 index 0000000..7f9c205 --- /dev/null +++ b/lib/data/datasource-view.mod.js @@ -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 index 0000000..55cb2d2 --- /dev/null +++ b/lib/data/index.js @@ -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 index 0000000..ba4ef66 --- /dev/null +++ b/lib/data/index.mod.js @@ -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 index 0000000..15f7c78 --- /dev/null +++ b/lib/data/metric-edit-view.js @@ -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 index 0000000..40d5442 --- /dev/null +++ b/lib/data/metric-edit-view.mod.js @@ -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 index 0000000..c304e53 --- /dev/null +++ b/lib/data/metric-model.js @@ -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 index 0000000..831825b --- /dev/null +++ b/lib/data/metric-model.mod.js @@ -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 index 0000000..eab642f --- /dev/null +++ b/lib/data/project-colors.js @@ -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 index 0000000..674a936 --- /dev/null +++ b/lib/data/project-colors.mod.js @@ -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 index 0000000..ed4246f --- /dev/null +++ b/lib/graph/graph-display-view.js @@ -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 index 0000000..76919e9 --- /dev/null +++ b/lib/graph/graph-display-view.mod.js @@ -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 index 0000000..2a562eb --- /dev/null +++ b/lib/graph/graph-edit-view.js @@ -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 index 0000000..ed45d38 --- /dev/null +++ b/lib/graph/graph-edit-view.mod.js @@ -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 index 0000000..03f37e4 --- /dev/null +++ b/lib/graph/graph-list-view.js @@ -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 index 0000000..b943a49 --- /dev/null +++ b/lib/graph/graph-list-view.mod.js @@ -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 index 0000000..cf04dbd --- /dev/null +++ b/lib/graph/graph-model.js @@ -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 index 0000000..3953385 --- /dev/null +++ b/lib/graph/graph-model.mod.js @@ -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 index 0000000..7c98892 --- /dev/null +++ b/lib/graph/graph-view.js @@ -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(' — '), + 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(' — '), + 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 index 0000000..c3bf4cb --- /dev/null +++ b/lib/graph/graph-view.mod.js @@ -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(' — '), + 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(' — '), + 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 index 0000000..8817c26 --- /dev/null +++ b/lib/graph/index.js @@ -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 index 0000000..f57db69 --- /dev/null +++ b/lib/graph/index.mod.js @@ -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 index 0000000..a9fabe2 --- /dev/null +++ b/lib/main-dashboard.js @@ -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 index 0000000..a497afc --- /dev/null +++ b/lib/main-display.js @@ -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 index 0000000..3d156ab --- /dev/null +++ b/lib/main-edit.js @@ -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 index 0000000..f406cd6 --- /dev/null +++ b/lib/main-geo.js @@ -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 index 0000000..79c0053 --- /dev/null +++ b/lib/main-graph-list.js @@ -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 index 0000000..a89b630 --- /dev/null +++ b/lib/template/browser-helpers.jade.js @@ -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 index 0000000..60be58f --- /dev/null +++ b/lib/template/chart/chart-option.jade.js @@ -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×\n ' + escape((interp = name) == null ? '' : interp) + '\n \n ' + escape((interp = name) == null ? '' : interp) + '\n '); +if ( ( /object|array|function/i.test(type) )) +{ +buf.push('\n ' + escape((interp = value) == null ? '' : interp) + '\n '); +} +else +{ + var input_type = (/boolean/i.test(type) ? 'checkbox' : 'text'); + var checked = ((/boolean/i.test(type) && value) ? 'checked' : null); +buf.push('\n '); +} +buf.push('\n ' + escape((interp = type) == null ? '' : interp) + '\n \n ' + escape((interp = def) == null ? '' : interp) + '\n \n '); +var __val__ = jade.filters.markdown(desc) +buf.push(null == __val__ ? "" : __val__); +buf.push('\n \n '); +// 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('' + escape((interp = tag) == null ? '' : interp) + ' \n'); + } + } else { + for (var $index in tags) { + var tag = tags[$index]; + + var tag_cls = _.domize('tag',tag) + ' ' + _.domize('category',model.getTagIndex(tag)) +buf.push('' + escape((interp = tag) == null ? '' : interp) + ' \n'); + } + } +}).call(this); + +buf.push('\n \n \n '); +// 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 ' + escape((interp = example) == null ? '' : interp) + '\n '); + } + } else { + for (var $index in examples) { + var example = examples[$index]; + +buf.push('\n ' + escape((interp = example) == null ? '' : interp) + '\n '); + } + } +}).call(this); + +buf.push('\n \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.mod.js b/lib/template/chart/chart-option.jade.mod.js new file mode 100644 index 0000000..7d3f16d --- /dev/null +++ b/lib/template/chart/chart-option.jade.mod.js @@ -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×\n ' + escape((interp = name) == null ? '' : interp) + '\n \n ' + escape((interp = name) == null ? '' : interp) + '\n '); +if ( ( /object|array|function/i.test(type) )) +{ +buf.push('\n ' + escape((interp = value) == null ? '' : interp) + '\n '); +} +else +{ + var input_type = (/boolean/i.test(type) ? 'checkbox' : 'text'); + var checked = ((/boolean/i.test(type) && value) ? 'checked' : null); +buf.push('\n '); +} +buf.push('\n ' + escape((interp = type) == null ? '' : interp) + '\n \n ' + escape((interp = def) == null ? '' : interp) + '\n \n '); +var __val__ = jade.filters.markdown(desc) +buf.push(null == __val__ ? "" : __val__); +buf.push('\n \n '); +// 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('' + escape((interp = tag) == null ? '' : interp) + ' \n'); + } + } else { + for (var $index in tags) { + var tag = tags[$index]; + + var tag_cls = _.domize('tag',tag) + ' ' + _.domize('category',model.getTagIndex(tag)) +buf.push('' + escape((interp = tag) == null ? '' : interp) + ' \n'); + } + } +}).call(this); + +buf.push('\n \n \n '); +// 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 ' + escape((interp = example) == null ? '' : interp) + '\n '); + } + } else { + for (var $index in examples) { + var example = examples[$index]; + +buf.push('\n ' + escape((interp = example) == null ? '' : interp) + '\n '); + } + } +}).call(this); + +buf.push('\n \n \n'); +} +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 index 0000000..49dc47e --- /dev/null +++ b/lib/template/chart/chart-scaffold.jade.js @@ -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\n Collapse AllExpand All\n StandardAdvanced\n \n \n \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-scaffold.jade.mod.js b/lib/template/chart/chart-scaffold.jade.mod.js new file mode 100644 index 0000000..abf742f --- /dev/null +++ b/lib/template/chart/chart-scaffold.jade.mod.js @@ -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\n Collapse AllExpand All\n StandardAdvanced\n \n \n \n \n'); +} +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 index 0000000..3fe6330 --- /dev/null +++ b/lib/template/dashboard/dashboard-tab.jade.js @@ -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\n'); +} +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 index 0000000..36f1df7 --- /dev/null +++ b/lib/template/dashboard/dashboard-tab.jade.mod.js @@ -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\n'); +} +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 index 0000000..afa3059 --- /dev/null +++ b/lib/template/dashboard/dashboard.jade.js @@ -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\n \n

    Wikimedia Report Card April 2012\n

    \n \n \n \n \n \n \n \n \n'); +} +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 index 0000000..0e58d87 --- /dev/null +++ b/lib/template/dashboard/dashboard.jade.mod.js @@ -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\n \n

    Wikimedia Report Card April 2012\n

    \n \n \n \n \n \n \n \n \n'); +} +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 index 0000000..7b270cd --- /dev/null +++ b/lib/template/data/data.jade.js @@ -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\n \n \n \n \n \n \n'); +} +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 index 0000000..b15812d --- /dev/null +++ b/lib/template/data/data.jade.mod.js @@ -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\n \n \n \n \n \n \n'); +} +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 index 0000000..8e2b249 --- /dev/null +++ b/lib/template/data/dataset-metric.jade.js @@ -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\n ' + escape((interp = label) == null ? '' : interp) + '\n \n ' + escape((interp = source) == null ? '' : interp) + '\n \n ' + escape((interp = timespan) == null ? '' : interp) + '\n \n ×\n \n \n \n \n \n'); +} +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 index 0000000..e36e2c5 --- /dev/null +++ b/lib/template/data/dataset-metric.jade.mod.js @@ -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\n ' + escape((interp = label) == null ? '' : interp) + '\n \n ' + escape((interp = source) == null ? '' : interp) + '\n \n ' + escape((interp = timespan) == null ? '' : interp) + '\n \n ×\n \n \n \n \n \n'); +} +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 index 0000000..1fab1fa --- /dev/null +++ b/lib/template/data/dataset.jade.js @@ -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\n \n \n

    Graph Data Set\n

    \n Add Metric\n\n \n \n \n \n \n Label\n \n Source\n \n Timespan\n \n Actions\n \n \n \n \n \n \n \n \n \n'); +} +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 index 0000000..ff5a992 --- /dev/null +++ b/lib/template/data/dataset.jade.mod.js @@ -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\n \n \n

    Graph Data Set\n

    \n Add Metric\n\n \n \n \n \n \n Label\n \n Source\n \n Timespan\n \n Actions\n \n \n \n \n \n \n \n \n \n'); +} +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 index 0000000..a8c8180 --- /dev/null +++ b/lib/template/data/datasource-ui.jade.js @@ -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\n \n \n \n
  • ' + escape((interp = source_summary) == null ? '' : interp) + ' \/\n
  • \n
  • ' + escape((interp = metric_summary) == null ? '' : interp) + ' \/\n
  • \n
  • ' + escape((interp = timespan_summary) == null ? '' : interp) + '\n
  • \n \n \n \n \n \n \n
  • \n
    Data Sources\n
    \n
  • '); +// 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 ' + escape((interp = ds.shortName) == null ? '' : interp) + '\n '); + } + } 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 ' + escape((interp = ds.shortName) == null ? '' : interp) + '\n '); + } + } +}).call(this); + +buf.push('\n \n '); +// 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 \n \n ' + escape((interp = ds.name) == null ? '' : interp) + '\n \n ' + escape((interp = ds.id) == null ? '' : interp) + '\n \n ' + escape((interp = ds.format) == null ? '' : interp) + '\n \n ' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n \n \n \n ' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n \n \n \n \n \n \n \n #\n \n Label\n \n Type\n \n \n \n '); +// 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } +}).call(this); + +buf.push('\n \n \n \n '); + } + } 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 \n \n ' + escape((interp = ds.name) == null ? '' : interp) + '\n \n ' + escape((interp = ds.id) == null ? '' : interp) + '\n \n ' + escape((interp = ds.format) == null ? '' : interp) + '\n \n ' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n \n \n \n ' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n \n \n \n \n \n \n \n #\n \n Label\n \n Type\n \n \n \n '); +// 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } +}).call(this); + +buf.push('\n \n \n \n '); + } + } +}).call(this); + +buf.push('\n \n \n \n \n \n'); +} +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 index 0000000..b25e11e --- /dev/null +++ b/lib/template/data/datasource-ui.jade.mod.js @@ -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\n \n \n \n
  • ' + escape((interp = source_summary) == null ? '' : interp) + ' \/\n
  • \n
  • ' + escape((interp = metric_summary) == null ? '' : interp) + ' \/\n
  • \n
  • ' + escape((interp = timespan_summary) == null ? '' : interp) + '\n
  • \n \n \n \n \n \n \n
  • \n
    Data Sources\n
    \n
  • '); +// 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 ' + escape((interp = ds.shortName) == null ? '' : interp) + '\n '); + } + } 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 ' + escape((interp = ds.shortName) == null ? '' : interp) + '\n '); + } + } +}).call(this); + +buf.push('\n \n '); +// 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 \n \n ' + escape((interp = ds.name) == null ? '' : interp) + '\n \n ' + escape((interp = ds.id) == null ? '' : interp) + '\n \n ' + escape((interp = ds.format) == null ? '' : interp) + '\n \n ' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n \n \n \n ' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n \n \n \n \n \n \n \n #\n \n Label\n \n Type\n \n \n \n '); +// 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } +}).call(this); + +buf.push('\n \n \n \n '); + } + } 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 \n \n ' + escape((interp = ds.name) == null ? '' : interp) + '\n \n ' + escape((interp = ds.id) == null ? '' : interp) + '\n \n ' + escape((interp = ds.format) == null ? '' : interp) + '\n \n ' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n \n \n \n ' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n \n \n \n \n \n \n \n #\n \n Label\n \n Type\n \n \n \n '); +// 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } +}).call(this); + +buf.push('\n \n \n \n '); + } + } +}).call(this); + +buf.push('\n \n \n \n \n \n'); +} +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 index 0000000..3e52647 --- /dev/null +++ b/lib/template/data/datasource.jade.js @@ -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\n \n \n ' + escape((interp = ds.name) == null ? '' : interp) + '\n \n ' + escape((interp = ds.id) == null ? '' : interp) + '\n \n ' + escape((interp = ds.format) == null ? '' : interp) + '\n \n ' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n \n \n \n ' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n \n \n \n \n \n \n \n #\n \n Label\n \n Type\n \n \n \n '); +// 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } +}).call(this); + +buf.push('\n \n \n \n \n'); +} +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 index 0000000..9f14057 --- /dev/null +++ b/lib/template/data/datasource.jade.mod.js @@ -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\n \n \n ' + escape((interp = ds.name) == null ? '' : interp) + '\n \n ' + escape((interp = ds.id) == null ? '' : interp) + '\n \n ' + escape((interp = ds.format) == null ? '' : interp) + '\n \n ' + escape((interp = ds.chart.chartType) == null ? '' : interp) + '\n \n \n \n ' + escape((interp = ds.timespan.start) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.end) == null ? '' : interp) + '\n \n ' + escape((interp = ds.timespan.step) == null ? '' : interp) + '\n \n \n \n \n \n \n \n #\n \n Label\n \n Type\n \n \n \n '); +// 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } 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 \n ' + escape((interp = m.idx) == null ? '' : interp) + '\n \n ' + escape((interp = m.label) == null ? '' : interp) + '\n \n ' + escape((interp = m.type) == null ? '' : interp) + '\n \n '); + } + } +}).call(this); + +buf.push('\n \n \n \n \n'); +} +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 index 0000000..32ef6c8 --- /dev/null +++ b/lib/template/data/metric-edit.jade.js @@ -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\n \n \n \n \n \n \n \n \n \n \n Delete\n\n \n \n \n \n \n'); +} +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 index 0000000..95fe254 --- /dev/null +++ b/lib/template/data/metric-edit.jade.mod.js @@ -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\n \n \n \n \n \n \n \n \n \n \n Delete\n\n \n \n \n \n \n'); +} +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 index 0000000..f32ee08 --- /dev/null +++ b/lib/template/graph/graph-display.jade.js @@ -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\n \n \n ' + escape((interp = callout.latest) == null ? '' : interp) + '\n \n ' + escape((interp = callout.year.dates) == null ? '' : interp) + '' + escape((interp = callout.year.value) == null ? '' : interp) + '\n \n ' + escape((interp = callout.month.dates) == null ? '' : interp) + '' + escape((interp = callout.month.value) == null ? '' : interp) + '\n \n \n

    ' + escape((interp = name) == null ? '' : interp) + '\n

    \n \n \n \n \n \n \n \n \n \n \n '); +var __val__ = jade.filters.markdown(desc) +buf.push(null == __val__ ? "" : __val__); +buf.push('\n \n \n \n \n \n '); +if ( IS_DEV) +{ +buf.push(' Export\n'); +} +buf.push('\n \n \n '); +var __val__ = jade.filters.markdown(notes) +buf.push(null == __val__ ? "" : __val__); +buf.push('\n \n \n'); +} +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 index 0000000..baa4084 --- /dev/null +++ b/lib/template/graph/graph-display.jade.mod.js @@ -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\n \n \n ' + escape((interp = callout.latest) == null ? '' : interp) + '\n \n ' + escape((interp = callout.year.dates) == null ? '' : interp) + '' + escape((interp = callout.year.value) == null ? '' : interp) + '\n \n ' + escape((interp = callout.month.dates) == null ? '' : interp) + '' + escape((interp = callout.month.value) == null ? '' : interp) + '\n \n \n

    ' + escape((interp = name) == null ? '' : interp) + '\n

    \n \n \n \n \n \n \n \n \n \n \n '); +var __val__ = jade.filters.markdown(desc) +buf.push(null == __val__ ? "" : __val__); +buf.push('\n \n \n \n \n \n '); +if ( IS_DEV) +{ +buf.push(' Export\n'); +} +buf.push('\n \n \n '); +var __val__ = jade.filters.markdown(notes) +buf.push(null == __val__ ? "" : __val__); +buf.push('\n \n \n'); +} +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 index 0000000..7b13171 --- /dev/null +++ b/lib/template/graph/graph-edit.jade.js @@ -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\n \n \n \n \n \n \n \n \n \n \n \n \n \n Redraw\n\n \n Revert\n Save\n Done\n\n \n \n \n
  • \n \n \n

    Graph\n

    \n
  • \n Info\n \n
  • Data\n
  • \n
  • Options\n
  • \n \n \n \n \n \n \n \n \n \n \n \n \n \n Slug\n \n \n \n The slug uniquely identifies this graph and will be displayed in the URL once saved.\n

    \n \n \n \n Size\n \n \n × \n\n \n Choosing \'auto\' will size the graph to the viewport bounds.\n

    \n \n \n \n \n Description\n \n \n\n A description of the graph.\n

    \n \n \n \n \n \n \n \n \n \n \n \n \n'); +} +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 index 0000000..ce48362 --- /dev/null +++ b/lib/template/graph/graph-edit.jade.mod.js @@ -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\n \n \n \n \n \n \n \n \n \n \n \n \n \n Redraw\n\n \n Revert\n Save\n Done\n\n \n \n \n
  • \n \n \n

    Graph\n

    \n
  • \n Info\n \n
  • Data\n
  • \n
  • Options\n
  • \n \n \n \n \n \n \n \n \n \n \n \n \n \n Slug\n \n \n \n The slug uniquely identifies this graph and will be displayed in the URL once saved.\n

    \n \n \n \n Size\n \n \n × \n\n \n Choosing \'auto\' will size the graph to the viewport bounds.\n

    \n \n \n \n \n Description\n \n \n\n A description of the graph.\n

    \n \n \n \n \n \n \n \n \n \n \n \n \n'); +} +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 index 0000000..986a0b7 --- /dev/null +++ b/lib/template/graph/graph-list.jade.js @@ -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\n \n

    Saved Graphs\n

    \n \n
      '); +// 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
    • ' + escape((interp = graph.get('name')) == null ? '' : interp) + '\n
    • '); + } + } else { + for (var $index in collection.models) { + var graph = collection.models[$index]; + +buf.push('\n
    • ' + escape((interp = graph.get('name')) == null ? '' : interp) + '\n
    • '); + } + } +}).call(this); + +buf.push('\n
    \n'); +} +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 index 0000000..9d93f7d --- /dev/null +++ b/lib/template/graph/graph-list.jade.mod.js @@ -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\n \n

    Saved Graphs\n

    \n \n
      '); +// 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
    • ' + escape((interp = graph.get('name')) == null ? '' : interp) + '\n
    • '); + } + } else { + for (var $index in collection.models) { + var graph = collection.models[$index]; + +buf.push('\n
    • ' + escape((interp = graph.get('name')) == null ? '' : interp) + '\n
    • '); + } + } +}).call(this); + +buf.push('\n
    \n'); +} +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 index 0000000..83f6ed6 --- /dev/null +++ b/lib/util/backbone.js @@ -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} 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 index 0000000..1ac7157 --- /dev/null +++ b/lib/util/backbone.mod.js @@ -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} 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 index 0000000..ab6a5d4 --- /dev/null +++ b/lib/util/cascade.js @@ -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 + * @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} The array of lookup dictionaries. + */ + prototype.getLookups = function(){ + return this._lookups; + }; + /** + * @returns {Array} 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 index 0000000..356c618 --- /dev/null +++ b/lib/util/cascade.mod.js @@ -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 + * @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} The array of lookup dictionaries. + */ + prototype.getLookups = function(){ + return this._lookups; + }; + /** + * @returns {Array} 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 index 0000000..7d57333 --- /dev/null +++ b/lib/util/event/index.js @@ -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 index 0000000..175958e --- /dev/null +++ b/lib/util/event/index.mod.js @@ -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 index 0000000..76b12a5 --- /dev/null +++ b/lib/util/event/ready-emitter.js @@ -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 index 0000000..19ecfbf --- /dev/null +++ b/lib/util/event/ready-emitter.mod.js @@ -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 index 0000000..e87ccd8 --- /dev/null +++ b/lib/util/event/waiting-emitter.js @@ -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 index 0000000..4f17042 --- /dev/null +++ b/lib/util/event/waiting-emitter.mod.js @@ -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 index 0000000..4da7c06 --- /dev/null +++ b/lib/util/formatters.js @@ -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 "" + whole + "" + fraction + "" + suffix + ""; + } +}; +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 index 0000000..04972b7 --- /dev/null +++ b/lib/util/formatters.mod.js @@ -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 "" + whole + "" + fraction + "" + suffix + ""; + } +}; +module.exports = exports = _fmt; + +}); diff --git a/lib/util/index.js b/lib/util/index.js new file mode 100644 index 0000000..2cc464f --- /dev/null +++ b/lib/util/index.js @@ -0,0 +1,43 @@ +var op, root, backbone, parser, Cascade, _, _ref, __slice = [].slice; +_ = exports._ = require('kraken