From 4104257499b337b9868bb640eb990d488e06dfa3 Mon Sep 17 00:00:00 2001 From: David Schoonover Date: Wed, 23 May 2012 16:30:11 -0700 Subject: [PATCH] Refactor of source modules, including: - dataset -> data - timeseries -> util/timeseries - scaffold -> base/scaffold - Adds templates/{chart,dashboard,data,graph} to match the structure of the modules - Deletes unused/empty modules and templates --- docs/internals/housekeeping.md | 9 +-- lib/base/scaffold/index.co | 3 + lib/base/scaffold/scaffold-model.co | 105 +++++++++++++++++ lib/base/scaffold/scaffold-view.co | 125 +++++++++++++++++++++ lib/chart/chart-option-view.co | 4 +- lib/dashboard/dashboard-view.co | 2 +- lib/data/data-view.co | 122 ++++++++++++++++++++ lib/data/dataset-model.co | 177 +++++++++++++++++++++++++++++ lib/data/dataset-view.co | 155 ++++++++++++++++++++++++++ lib/data/datasource-model.co | 190 +++++++++++++++++++++++++++++++ lib/data/datasource-ui-view.co | 69 ++++++++++++ lib/data/datasource-view.co | 24 ++++ lib/data/index.co | 13 ++ lib/data/metric-edit-view.co | 94 ++++++++++++++++ lib/data/metric-model.co | 159 ++++++++++++++++++++++++++ lib/dataset/data-view.co | 122 -------------------- lib/dataset/dataset-model.co | 177 ----------------------------- lib/dataset/dataset-view.co | 155 -------------------------- lib/dataset/datasource-model.co | 190 ------------------------------- lib/dataset/datasource-ui-view.co | 69 ------------ lib/dataset/datasource-view.co | 24 ---- lib/dataset/index.co | 14 --- lib/dataset/metric-edit-view.co | 94 ---------------- lib/dataset/metric-model.co | 159 -------------------------- lib/graph/graph-display-view.co | 2 +- lib/graph/graph-edit-view.co | 4 +- lib/graph/graph-list-view.co | 2 +- lib/graph/graph-model.co | 2 +- lib/main-edit.co | 2 +- lib/scaffold/index.co | 3 - lib/scaffold/scaffold-model.co | 105 ----------------- lib/scaffold/scaffold-view.co | 125 --------------------- lib/template/chart-option.jade | 41 ------- lib/template/chart-scaffold.jade | 12 -- lib/template/chart/chart-option.jade | 41 +++++++ lib/template/chart/chart-scaffold.jade | 12 ++ lib/template/dashboard.jade | 22 ---- lib/template/dashboard/dashboard.jade | 22 ++++ lib/template/data.jade | 4 - lib/template/data/data.jade | 4 + lib/template/data/dataset-metric.jade | 8 ++ lib/template/data/dataset.jade | 24 ++++ lib/template/data/datasource-ui.jade | 55 +++++++++ lib/template/data/datasource.jade | 29 +++++ lib/template/data/metric-edit.jade | 22 ++++ lib/template/dataset-metric.jade | 8 -- lib/template/dataset.jade | 24 ---- lib/template/datasource-ui.jade | 55 --------- lib/template/datasource.jade | 29 ----- lib/template/graph-display.jade | 29 ----- lib/template/graph-edit.jade | 75 ------------- lib/template/graph-list.jade | 7 - lib/template/graph/graph-display.jade | 29 +++++ lib/template/graph/graph-edit.jade | 75 +++++++++++++ lib/template/graph/graph-list.jade | 7 + lib/template/metric-edit.jade | 22 ---- lib/timeseries/csv.co | 115 ------------------- lib/timeseries/index.co | 2 - lib/timeseries/timeseries.co | 191 -------------------------------- lib/util/timeseries/csv.co | 115 +++++++++++++++++++ lib/util/timeseries/index.co | 2 + lib/util/timeseries/timeseries.co | 191 ++++++++++++++++++++++++++++++++ www/misc/test.co | 2 +- www/modules.yaml | 64 ++++++----- 64 files changed, 1916 insertions(+), 1922 deletions(-) create mode 100644 lib/base/scaffold/index.co create mode 100644 lib/base/scaffold/scaffold-model.co create mode 100644 lib/base/scaffold/scaffold-view.co create mode 100644 lib/data/data-view.co create mode 100644 lib/data/dataset-model.co create mode 100644 lib/data/dataset-view.co create mode 100644 lib/data/datasource-model.co create mode 100644 lib/data/datasource-ui-view.co create mode 100644 lib/data/datasource-view.co create mode 100644 lib/data/index.co create mode 100644 lib/data/metric-edit-view.co create mode 100644 lib/data/metric-model.co delete mode 100644 lib/dataset/data-view.co delete mode 100644 lib/dataset/dataset-model.co delete mode 100644 lib/dataset/dataset-view.co delete mode 100644 lib/dataset/datasource-model.co delete mode 100644 lib/dataset/datasource-ui-view.co delete mode 100644 lib/dataset/datasource-view.co delete mode 100644 lib/dataset/index.co delete mode 100644 lib/dataset/metric-edit-view.co delete mode 100644 lib/dataset/metric-model.co delete mode 100644 lib/dataset/metric-view.co delete mode 100644 lib/scaffold/index.co delete mode 100644 lib/scaffold/scaffold-model.co delete mode 100644 lib/scaffold/scaffold-view.co delete mode 100644 lib/template/chart-option.jade delete mode 100644 lib/template/chart-scaffold.jade create mode 100644 lib/template/chart/chart-option.jade create mode 100644 lib/template/chart/chart-scaffold.jade delete mode 100644 lib/template/dashboard.jade create mode 100644 lib/template/dashboard/dashboard.jade delete mode 100644 lib/template/data.jade create mode 100644 lib/template/data/data.jade create mode 100644 lib/template/data/dataset-metric.jade create mode 100644 lib/template/data/dataset.jade create mode 100644 lib/template/data/datasource-ui.jade create mode 100644 lib/template/data/datasource.jade create mode 100644 lib/template/data/metric-edit.jade delete mode 100644 lib/template/dataset-metric.jade delete mode 100644 lib/template/dataset.jade delete mode 100644 lib/template/datasource-ui.jade delete mode 100644 lib/template/datasource.jade delete mode 100644 lib/template/graph-display.jade delete mode 100644 lib/template/graph-edit.jade delete mode 100644 lib/template/graph-list.jade create mode 100644 lib/template/graph/graph-display.jade create mode 100644 lib/template/graph/graph-edit.jade create mode 100644 lib/template/graph/graph-list.jade delete mode 100644 lib/template/index.co delete mode 100644 lib/template/metric-edit.jade delete mode 100644 lib/timeseries/csv.co delete mode 100644 lib/timeseries/index.co delete mode 100644 lib/timeseries/timeseries.co create mode 100644 lib/util/timeseries/csv.co create mode 100644 lib/util/timeseries/index.co create mode 100644 lib/util/timeseries/timeseries.co diff --git a/docs/internals/housekeeping.md b/docs/internals/housekeeping.md index 978169b..c464fd2 100644 --- a/docs/internals/housekeeping.md +++ b/docs/internals/housekeeping.md @@ -49,15 +49,8 @@ ## Misc - Generate markdoc wiki from `/docs` on Build, Deploy - Refactor directories: - - template/ - graph/ - chart/ - data/ - dashboard/ - - dataset/ -> data/ + - dataset/ dataset/ +metric datasource/ +column - - scaffold/ -> base/scaffold/ - - timeseries/ -> util/timeseries/ - Wrap `Backbone.extend()` to fire `subclass` event on parent class diff --git a/lib/base/scaffold/index.co b/lib/base/scaffold/index.co new file mode 100644 index 0000000..6524ae6 --- /dev/null +++ b/lib/base/scaffold/index.co @@ -0,0 +1,3 @@ +models = require 'kraken/base/scaffold/scaffold-model' +views = require 'kraken/base/scaffold/scaffold-view' +exports import models import views diff --git a/lib/base/scaffold/scaffold-model.co b/lib/base/scaffold/scaffold-model.co new file mode 100644 index 0000000..2800782 --- /dev/null +++ b/lib/base/scaffold/scaffold-model.co @@ -0,0 +1,105 @@ +_ = require 'kraken/util/underscore' +op = require 'kraken/util/op' +{ BaseModel, BaseList, +} = require 'kraken/base' + + + +### Scaffold Models + +Field = exports.Field = BaseModel.extend do # {{{ + valueAttribute : 'value' + + defaults: -> + name : '' + type : 'String' + default : null + desc : '' + include : 'diff' + tags : [] + examples : [] + + + + constructor: function Field + BaseModel ... + + initialize: -> + _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse')) + @set 'id', @id = _.camelize @get 'name' + @set 'value', @get('default'), {+silent} if not @has 'value' + Field.__super__.initialize ... + + + + + + + /* * * Value Accessors * * */ + + getValue: (def) -> + @getParser() @get @valueAttribute, def + + setValue: (v, options) -> + def = @get 'default' + if not v and def == null + val = null + else + val = @getParser()(v) + @set @valueAttribute, val, options + + clearValue: -> + @set @valueAttribute, @get 'default' + + isDefault: -> + @get(@valueAttribute) is @get 'default' + + + /* * * Serializers * * */ + + serializeValue: -> + @serialize @getValue() + + toJSON: -> + {id:@id} import do + _.clone(@attributes) import { value:@getValue(), def:@get 'default' } + + toKVPairs: -> + { "#{@id}":@serializeValue() } + + toString: -> "(#{@id}: #{@serializeValue()})" + +# }}} + + +FieldList = exports.FieldList = BaseList.extend do # {{{ + model : Field + + constructor: function FieldList + BaseList ... + + + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */ + values: (opts={}) -> + opts = {+keepDefaults, -serialize} import opts + _.synthesize do + if opts.keepDefaults then @models else @models.filter -> not it.isDefault() + -> [ it.get('name'), if opts.serialize then it.serializeValue() else it.getValue() ] + + toJSON: -> + @values {+keepDefaults, -serialize} + + toKVPairs: -> + _.collapseObject @values {+keepDefaults, +serialize} + + toKV: (item_delim='&', kv_delim='=') -> + _.toKV @toKVPairs(), item_delim, kv_delim + + toURL: (item_delim='&', kv_delim='=') -> + "?#{@toKV ...}" + +# }}} + diff --git a/lib/base/scaffold/scaffold-view.co b/lib/base/scaffold/scaffold-view.co new file mode 100644 index 0000000..b5299bc --- /dev/null +++ b/lib/base/scaffold/scaffold-view.co @@ -0,0 +1,125 @@ +_ = require 'kraken/util/underscore' +op = require 'kraken/util/op' +{ BaseView, +} = require 'kraken/base' +{ Field, FieldList, +} = require 'kraken/base/scaffold/scaffold-model' + + +FieldView = exports.FieldView = BaseView.extend do # {{{ + tagName : 'div' + className : 'field' + + type : 'string' + + events : + 'blur .value' : 'onChange' + 'submit .value' : 'onChange' + + + constructor: function FieldView + BaseView ... + + initialize: -> + # console.log "#this.initialize!" + BaseView::initialize ... + @type = @model.get 'type' .toLowerCase() or 'string' + + onChange: -> + if @type is 'boolean' + val = !! @$('.value').attr('checked') + else + val = @model.getParser() @$('.value').val() + + current = @model.getValue() + return if _.isEqual val, current + # console.log "#this.onChange( #current -> #val )" + @model.setValue val, {+silent} + @trigger 'change', this + + toTemplateLocals: -> + json = FieldView.__super__.toTemplateLocals ... + json.id or= _.camelize json.name + json.value ?= '' + json.value = JSON.stringify v if v = json.value and (_.isArray(v) or _.isPlainObject(v)) + json + + /** + * A ghetto default template, typically overridden by superclass. + */ + template: (locals) -> + $ """ + + + """ + + render: -> + return @remove() if @model.get 'ignore' + FieldView.__super__.render ... + +# }}} + + +# There are several special options that, if passed, will be attached directly to the view: +# model, collection, el, id, className, tagName, attributes + +Scaffold = exports.Scaffold = BaseView.extend do # {{{ + __bind__ : <[ addField resetFields ]> + tagName : 'form' + className : 'scaffold' + + collectionType : FieldList + subviewType : FieldView + + + + constructor: function Scaffold + BaseView ... + + initialize: -> + CollectionType = @collectionType + @model = (@collection or= new CollectionType) + BaseView::initialize ... + + @collection.on 'add', @addField, this + @collection.on 'reset', @resetFields, this + + + + addField: (field) -> + @removeSubview field.view if field.view + + # avoid duplicating event propagation + field.off 'change:value', @onChange, this + + # propagate value-change events as key-value change events + field.on 'change:value', @onChange, this + + SubviewType = @subviewType + view = @addSubview new SubviewType model:field + view.on 'change', @onChange.bind(this, field) + + @render() + view + + resetFields: -> + @removeAllSubviews() + @collection.each @addField + this + + onChange: (field) -> + key = field.get 'name' + value = field.getValue() + @trigger "change:#key", this, value, key, field + @trigger "change", this, value, key, field + this + + + +# Proxy collection methods +<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]> + .forEach (methodname) -> + Scaffold::[methodname] = -> @collection[methodname].apply @collection, arguments + +# }}} + diff --git a/lib/chart/chart-option-view.co b/lib/chart/chart-option-view.co index 1fde054..6c9e2a0 100644 --- a/lib/chart/chart-option-view.co +++ b/lib/chart/chart-option-view.co @@ -14,7 +14,7 @@ DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms ChartOptionView = exports.ChartOptionView = BaseView.extend do # {{{ tagName : 'section' className : 'chart-option field' - template : require 'kraken/template/chart-option' + template : require 'kraken/template/chart/chart-option' type : 'string' isCollapsed : true @@ -129,7 +129,7 @@ ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend do # {{{ __bind__ : <[ addField ]> tagName : 'form' className : 'chart-options scaffold' - template : require 'kraken/template/chart-scaffold' + template : require 'kraken/template/chart/chart-scaffold' collectionType : ChartOptionList subviewType : ChartOptionView diff --git a/lib/dashboard/dashboard-view.co b/lib/dashboard/dashboard-view.co index 6fc4d59..fc0658c 100644 --- a/lib/dashboard/dashboard-view.co +++ b/lib/dashboard/dashboard-view.co @@ -17,7 +17,7 @@ DashboardView = exports.DashboardView = BaseView.extend do # {{{ __bind__ : <[ ]> tagName : 'section' className : 'dashboard' - template : require 'kraken/template/dashboard' + template : require 'kraken/template/dashboard/dashboard' graph_ids : <[ unique_visitors diff --git a/lib/data/data-view.co b/lib/data/data-view.co new file mode 100644 index 0000000..c57eaa8 --- /dev/null +++ b/lib/data/data-view.co @@ -0,0 +1,122 @@ +Seq = require 'seq' +{ _, op, +} = require 'kraken/util' +{ BaseView, ViewList, +} = require 'kraken/base' +{ DataSetView, +} = require 'kraken/data/dataset-view' +{ MetricEditView, +} = require 'kraken/data/metric-edit-view' +{ DataSource, +} = require 'kraken/data/datasource-model' + +/** + * @class DataSet selection and customization UI (root of the `data` tab). + */ +DataView = exports.DataView = BaseView.extend do # {{{ + __bind__ : <[ onMetricsChanged ]> + tagName : 'section' + className : 'data-ui' + template : require 'kraken/template/data/data' + + datasources : null + + + /** + * @constructor + */ + constructor: function DataView + BaseView ... + + initialize: -> + @graph_id = @options.graph_id + BaseView::initialize ... + @metric_views = new ViewList + @datasources = DataSource.getAllSources() + # @on 'update', @onUpdate, this + @model.metrics + .on 'add', @addMetric, this + .on 'remove', @removeMetric, this + @model.once 'ready', @onReady, this + + onReady: -> + # console.log "#this.onReady! #{@model.metrics}" + dataset = @model + @model.metrics.each @addMetric, this + @dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources} + @addSubview @dataset_view + .on 'add-metric', @onMetricsChanged, this + .on 'remove-metric', @onMetricsChanged, this + .on 'select-metric', @selectMetric, this + + @render() + @triggerReady() + this + + + /** + * Transform the `columns` field to ensure an Array of {label, type} objects. + */ + canonicalizeDataSource: (ds) -> + ds.shortName or= ds.name + ds.title or= ds.name + ds.subtitle or= '' + + cols = ds.columns + if _.isArray cols + ds.metrics = _.map cols, (col, idx) -> + if _.isArray col + [label, type] = col + {idx, label, type or 'int'} + else + col + else + ds.metrics = _.map cols.labels, (label, idx) -> + {idx, label, type:cols.types[idx] or 'int'} + ds + + + toTemplateLocals: -> + attrs = _.clone @model.attributes + { @graph_id, @datasources } import attrs + + addMetric: (metric) -> + # console.log "#this.addMetric!", metric + return metric if @metric_views.findByModel metric + view = new MetricEditView {model:metric, @graph_id, dataset:@model, @datasources} + .on 'metric-update', @onUpdateMetric, this + .on 'metric-change', @onUpdateMetric, this + @metric_views.push @addSubview view + @renderSubviews() + metric + + removeMetric: (metric) -> + # console.log "#this.removeMetric!", metric + return unless view = @metric_views.findByModel metric + @metric_views.remove view + @removeSubview view + metric + + selectMetric: (metric) -> + # console.log "#this.selectMetric!", metric + @metric_views.invoke 'hide' + @metric_edit_view = @metric_views.findByModel metric + @metric_edit_view?.show() + _.delay @onMetricsChanged, 10 + + onMetricsChanged: -> + return unless @dataset_view + oldMinHeight = parseInt @$el.css 'min-height' + newMinHeight = Math.max do + @dataset_view.$el.height() + @metric_edit_view?.$el.height() + # console.log 'onMetricsChanged!', oldMinHeight, '-->', newMinHeight + @$el.css 'min-height', newMinHeight + + onUpdateMetric: -> + # console.log "#this.onUpdateMetric!" + @trigger 'metric-change', @model, this + @render() + + +# }}} diff --git a/lib/data/dataset-model.co b/lib/data/dataset-model.co new file mode 100644 index 0000000..b96480b --- /dev/null +++ b/lib/data/dataset-model.co @@ -0,0 +1,177 @@ +Seq = require 'seq' +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ BaseModel, BaseList, +} = require 'kraken/base' +{ Metric, MetricList, +} = require 'kraken/data/metric-model' +{ DataSource, DataSourceList, +} = require 'kraken/data/datasource-model' + + + +/** + * @class + */ +DataSet = exports.DataSet = BaseModel.extend do # {{{ + urlRoot : '/datasets' + + /** + * @type DataSourceList + */ + sources : null + + /** + * @type MetricList + */ + metrics : null + + defaults : -> + palette : null + lines : [] + metrics : [] + + + constructor: function DataSet (attributes={}, opts) + @metrics = new MetricList attributes.metrics + BaseModel.call this, attributes, opts + + initialize : -> + BaseModel::initialize ... + @set 'metrics', @metrics, {+silent} + @on 'change:metrics', @onMetricChange, this + # @metrics.on 'add remove reset', ~> + # @trigger 'change:metrics', @metrics, this + + + load: (opts={}) -> + @resetReady() if opts.force + return this if @loading or @ready + + unless @metrics.length + return @triggerReady() + + # console.log "#this.load()..." + @wait() + @loading = true + @trigger 'load', this + Seq @metrics.models + .parEach_ (next, metric) -> + metric.once 'ready', next.ok .load() + .seq ~> + # console.log "#{this}.load() complete!" + @loading = false + @unwait() # terminates the `load` wait + @triggerReady() + this + + # refreshSubModels: -> + # # @set 'metrics', @metrics.toJSON(), {+silent} + # @set 'metrics', _.pluck(@metrics.models, 'attributes'), {+silent} + # this + + /** + * Override to handle the case where one of our rich sub-objects + * (basically `metrics`) is set as a result of the `fetch()` call by the + * Graph object. To prevent it from blowing away the `MetricList`, we + * perform a `reset()` here. But that won't trigger a `change:metrics` event, + * so we do a little dance to set it twice, as object identity would otherwise + * cause it to think nothing has changed. + */ + set: (key, value, opts) -> + # return DataSet.__super__.set ... unless @metrics + + if _.isObject(key) and key? + [values, opts] = [key, value] + else + values = { "#key": value } + opts or= {} + + for key, value in values + continue unless key is 'metrics' and _.isArray value + @metrics.reset value + delete values[key] + unless opts.silent + DataSet.__super__.set.call this, 'metrics', value, {+silent} + DataSet.__super__.set.call this, 'metrics', @metrics, opts + + DataSet.__super__.set.call this, values, opts + + + /* * * * TimeSeriesData interface * * * {{{ */ + + /** + * @returns {Array} The reified dataset, materialized to a list of rows including timestamps. + */ + getData: -> + return [] unless @ready + columns = @getColumns() + if columns?.length + _.zip ...columns + else + [] + + /** + * @returns {Array} List of all columns (including date column). + */ + getColumns: -> + return [] unless @ready + _.compact [ @getDateColumn() ].concat @getDataColumns() + + /** + * @returns {Array} The date column. + */ + getDateColumn: -> + return [] unless @ready + dates = @metrics.onlyOk().invoke 'getDateColumn' + maxLen = _.max _.pluck dates, 'length' + _.find dates, -> it.length is maxLen + + /** + * @returns {Array} List of all columns except the date column. + */ + getDataColumns: -> + return [] unless @ready + @metrics.onlyOk().invoke 'getData' + + /** + * @returns {Array} List of column labels. + */ + getLabels: -> + return [] unless @ready + [ 'Date' ].concat @metrics.onlyOk().invoke 'getLabel' + + getColors: -> + return [] unless @ready + @metrics.onlyOk().pluck 'color' + + # }}} + + + newMetric: -> + index = @metrics.length + @metrics.add m = new Metric { index, color:ColorBrewer.Spectral[11][index] } + m.on 'ready', ~> @trigger 'metric-data-loaded', this, m + # @trigger 'change:metrics', this, @metrics, 'metrics' + # @trigger 'change', this, @metrics, 'metrics' + m + + onMetricChange: -> + # console.log "#this.onMetricChange! ready=#{@ready}" + @resetReady() + @load() + + + # XXX: toJSON() must ensure columns in MetricList are ordered by index + # ...in theory, MetricList.comparator now does this + + # toJSON: -> + # @refreshSubModels() + # json = DataSet.__super__.toJSON ... + # json.metrics = json.metrics.map -> it.toJSON?() or it + # json + +# }}} + diff --git a/lib/data/dataset-view.co b/lib/data/dataset-view.co new file mode 100644 index 0000000..4a55a14 --- /dev/null +++ b/lib/data/dataset-view.co @@ -0,0 +1,155 @@ +{ _, op, +} = require 'kraken/util' +{ BaseView, +} = require 'kraken/base' + + +/** + * @class + */ +DataSetView = exports.DataSetView = BaseView.extend do # {{{ + tagName : 'section' + className : 'dataset-ui dataset' + template : require 'kraken/template/data/dataset' + + events: + 'click .new-metric-button' : 'onNewMetric' + 'click .delete-metric-button' : 'onDeleteMetric' + 'click .metrics .dataset-metric' : 'selectMetric' + + views_by_cid : {} + active_view : null + + + constructor: function DataSetView + BaseView ... + + initialize: -> + {@graph_id, @datasources, @dataset} = @options + BaseView::initialize ... + @views_by_cid = {} + @model + .on 'ready', @addAllMetrics, this + @model.metrics + .on 'add', @addMetric, this + .on 'remove', @removeMetric, this + .on 'change', @onMetricChange, this + .on 'reset', @addAllMetrics, this + + + addMetric: (metric) -> + # console.log "#this.addMetric!", metric + if @views_by_cid[metric.cid] + @removeSubview that + delete @views_by_cid[metric.cid] + + view = @addSubview new DataSetMetricView {model:metric, @graph_id} + @views_by_cid[metric.cid] = view + @trigger 'add-metric', metric, view, this + @render() + view + + removeMetric: (metric) -> + if metric instanceof [jQuery.Event, Event] + metric = @getMetricForElement metric.target + # console.log "#this.removeMetric!", metric + return unless metric + if view = @views_by_cid[metric.cid] + @removeSubview view + delete @views_by_cid[metric.cid] + @trigger 'remove-metric', metric, view, this + view + + addAllMetrics: -> + # console.log "#this.addAllMetrics! --> #{@model.metrics}" + @removeAllSubviews() + @model.metrics.each @addMetric, this + this + + + selectMetric: (metric) -> + if metric instanceof [jQuery.Event, Event] + metric = @getMetricForElement metric.target + # console.log "#this.selectMetric!", metric + return unless metric + view = @active_view = @views_by_cid[metric.cid] + + @$ '.metrics .dataset-metric' .removeClass 'metric-active' + view.$el.addClass 'metric-active' + view.$ '.activity-arrow' .css 'font-size', 2+view.$el.height() + + @trigger 'select-metric', metric, view, this + this + + onMetricChange: (metric) -> + return unless view = @views_by_cid[metric?.cid] + view.$ '.activity-arrow:visible' .css 'font-size', 2+view.$el.height() + + onNewMetric: -> + # console.log "#this.newMetric!" + # triggers 'add' on @model.metrics + @model.newMetric() + false + + onDeleteMetric: (evt) -> + metric = @getMetricForElement evt.target + # console.log "#this.onDeleteMetric!", metric + # Triggers a 'remove' event, which in turn calls `removeMetric()` + @model.metrics.remove metric + false + + + getMetricForElement: (el) -> + $ el .parents '.dataset-metric' .eq(0).data 'model' + +# }}} + + + +/** + * @class + */ +DataSetMetricView = exports.DataSetMetricView = BaseView.extend do # {{{ + tagName : 'tr' + className : 'dataset-metric metric' + template : require 'kraken/template/data/dataset-metric' + + + + constructor: function DataSetMetricView + BaseView ... + + initialize: -> + @graph_id = @options.graph_id + BaseView::initialize ... + @on 'update', @onUpdate, this + + + toTemplateLocals: -> + m = DataSetMetricView.__super__.toTemplateLocals ... + + # XXX: Icons/classes for visible/disabled? + m import + graph_id : @graph_id + label : @model.getLabel() + viewClasses : _.compact([ + if @model.isOk() then 'valid' else 'invalid', + if m.visible then 'visible' else 'hidden', + 'disabled' if m.disabled, + ]).map( -> "metric-#it" ).join ' ' + source : + if m.source_id and m.source_col + "#{m.source_id}[#{m.source_col}]" + else + 'No source' + timespan : + if _.every ts = m.timespan, op.ok + "#{ts.start} to #{ts.end} by #{ts.step}" + else + '—' + + onUpdate: -> + @$ '.col-color' .css 'color', @model.get 'color' + +# }}} + diff --git a/lib/data/datasource-model.co b/lib/data/datasource-model.co new file mode 100644 index 0000000..490ede7 --- /dev/null +++ b/lib/data/datasource-model.co @@ -0,0 +1,190 @@ +{ _, op, +} = require 'kraken/util' +{ TimeSeriesData, CSVData, +} = require 'kraken/util/timeseries' +{ BaseModel, BaseList, ModelCache, +} = require 'kraken/base' +{ Metric, MetricList, +} = require 'kraken/data/metric-model' + + +/** + * @class + */ +DataSource = exports.DataSource = BaseModel.extend do # {{{ + __bind__ : <[ onLoadDataSuccess onLoadDataError ]> + urlRoot : '/datasources' + ready : false + + /** + * Parsed data for this datasource. + * @type Array + */ + data : null + + defaults: -> + id : '' + url : '' + format : 'json' + + name : '' + shortName : '' + title : '' + subtitle : '' + desc : '' + notes : '' + + timespan : + start : null + end : null + step : '1mo' + + columns : [] + + chart : + chartType : 'dygraphs' + options : {} + + url: -> + "/datasources/#{@id}.json" + + + + + + constructor: function DataSource + BaseModel ... + + initialize: -> + @attributes = @canonicalize @attributes + BaseModel::initialize ... + @constructor.register this + @metrics = new MetricList @attributes.metrics + @on 'change:metrics', @onMetricChange, this + + + canonicalize: (ds) -> + ds.shortName or= ds.name + ds.title or= ds.name + ds.subtitle or= '' + + cols = ds.columns + if _.isArray cols + ds.metrics = _.map cols, (col, idx) -> + if _.isArray col + [label, type] = col + {idx, label, type or 'int'} + else + col.type or= 'int' + col + else + ds.metrics = _.map cols.labels, (label, idx) -> + {idx, label, type:cols.types[idx] or 'int'} + ds + + + + loadAll: -> + @loader start: -> + Seq() + .seq_ (next) ~> + @once 'fetch-success', next.ok + @loadModel() + .seq_ (next) ~> + @once 'load-data-success', next.ok + @loadData() + .seq ~> + @trigger 'load-success', this + this + + loadData: -> + @wait() + @trigger 'load-data', this + return @onLoadDataSuccess @data if @data + switch @get 'format' + case 'json' then @loadJSON() + case 'csv' then @loadCSV() + default + console.error "#this.load() Unknown Data Format!" + @onLoadDataError null, 'Unknown Data Format!', new Error 'Unknown Data Format!' + this + + loadJSON: -> + $.ajax do + url : @get 'url' + dataType : 'json' + success : (data) ~> @onLoadDataSuccess new TimeSeriesData data + error : @onLoadDataError + this + + loadCSV: -> + $.ajax do + url : @get 'url' + dataType : 'text' + success : (data) ~> @onLoadDataSuccess new CSVData data + error : @onLoadDataError + this + + onLoadDataSuccess: (@data) -> + console.log "#this.onLoadDataSuccess #{@data}" + @unwait() + @trigger 'load-data-success', this + @triggerReady() + + onLoadDataError: (jqXHR, txtStatus, err) -> + console.error "#this Error loading data! -- #msg: #{err or ''}" + @unwait() + @_errorLoading = true + @trigger 'load-data-error', this, txtStatus, err + + + getDateColumn: -> + @data.dateColumn + + getData: -> + @data.toJSON?() or @data + + getColumn: (idx) -> + @data.columns[idx] + + getColumnName: (idx) -> + @get('metrics')?[idx]?.label + + getColumnIndex: (name) -> + return that.idx if _.find @get('metrics'), -> it.label is name + -1 + + onMetricChange: -> + @metrics.reset @get 'metrics' + + +# }}} + + +/** + * @class + */ +DataSourceList = exports.DataSourceList = BaseList.extend do # {{{ + urlRoot : '/datasources' + model : DataSource + + constructor: function DataSourceList then BaseList ... + initialize : -> BaseList::initialize ... +# }}} + + + +### DataSource Cache + +ALL_SOURCES = new DataSourceList +sourceCache = new ModelCache DataSource, {-ready, cache:ALL_SOURCES} + +# Fetch all DataSources +$.getJSON '/datasources/all', (data) -> + ALL_SOURCES.reset _.map data, op.I + sourceCache.triggerReady() + +DataSource.getAllSources = -> + ALL_SOURCES + + diff --git a/lib/data/datasource-ui-view.co b/lib/data/datasource-ui-view.co new file mode 100644 index 0000000..7f04041 --- /dev/null +++ b/lib/data/datasource-ui-view.co @@ -0,0 +1,69 @@ +{ _, op, +} = require 'kraken/util' +{ BaseModel, BaseList, BaseView, +} = require 'kraken/base' + + +/** + * @class + * Model is a Metric. + */ +DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{ + __bind__ : <[ ]> + tagName : 'section' + className : 'datasource-ui' + template : require 'kraken/template/data/datasource-ui' + + events : + 'click .datasource-summary' : 'onHeaderClick' + 'click .datasource-source-metric' : 'onSelectMetric' + + graph_id : null + dataset : null + datasources : null + + + + constructor: function DataSourceUIView + BaseView ... + + initialize: -> + this import @options.{graph_id, dataset, datasources} + BaseView::initialize ... + + toTemplateLocals: -> + locals = @model.toJSON() + locals import {@graph_id, @dataset, @datasources, cid:@model.cid} + + ds = @model.source + hasSource = @model.get('source_id')? and ds + locals.source_summary = unless hasSource then '' else @model.getSourceColumnName() + + dsts = ds?.get('timespan') or {} + ts = locals.timespan = _.defaults _.clone(@model.get('timespan')), dsts + hasTimespan = hasMetric and ts.start and ts.end and ts.step + locals.timespan_summary = unless hasTimespan then '' else ds.get 'shortName' - - hasMetric = hasSource and @model.get('source_col')? - locals.metric_summary = unless hasMetric then '' else "#{ts.start} — #{ts.end}" - - locals - - onHeaderClick: -> - @$el.toggleClass 'in' - - onSelectMetric: (evt) -> - # tr = evt.currentTarget - # idx = @$ '.source-metrics .datasource-source-metric' .toArray().indexOf tr - # return unless idx is not -1 - el = $ evt.currentTarget - {source_id, source_col} = el.data() - source_col = parseInt source_col - return if not source_id or isNaN source_col - - @$ '.source-metrics .datasource-source-metric' .removeClass 'active' - el.addClass 'active' - @model.set {source_col, source_id} - @trigger 'metric-change', @model, this - -# }}} diff --git a/lib/dataset/datasource-view.co b/lib/dataset/datasource-view.co deleted file mode 100644 index 57dc290..0000000 --- a/lib/dataset/datasource-view.co +++ /dev/null @@ -1,24 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseModel, BaseList, BaseView, -} = require 'kraken/base' - - -/** - * @class - */ -DataSourceView = exports.DataSourceView = BaseView.extend do # {{{ - __bind__ : <[ ]> - tagName : 'section' - className : 'datasource' - template : require 'kraken/template/datasource' - - - - constructor: function DataSourceView - BaseView ... - - initialize: -> - BaseView::initialize ... - -# }}} diff --git a/lib/dataset/index.co b/lib/dataset/index.co deleted file mode 100644 index 56d3e89..0000000 --- a/lib/dataset/index.co +++ /dev/null @@ -1,14 +0,0 @@ -metric_model = require 'kraken/dataset/metric-model' -metric_view = require 'kraken/dataset/metric-view' -metric_edit_view = require 'kraken/dataset/metric-edit-view' -datasource_model = require 'kraken/dataset/datasource-model' -datasource_view = require 'kraken/dataset/datasource-view' -datasource_ui_view = require 'kraken/dataset/datasource-ui-view' -dataset_model = require 'kraken/dataset/dataset-model' -dataset_view = require 'kraken/dataset/dataset-view' -data_view = require 'kraken/dataset/data-view' - -exports import metric_model import metric_view import metric_edit_view \ - import datasource_model import datasource_view import datasource_ui_view \ - import dataset_model import dataset_view \ - import data_view diff --git a/lib/dataset/metric-edit-view.co b/lib/dataset/metric-edit-view.co deleted file mode 100644 index f1b0fef..0000000 --- a/lib/dataset/metric-edit-view.co +++ /dev/null @@ -1,94 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseView, -} = require 'kraken/base' -{ Metric, -} = require 'kraken/dataset/metric-model' -{ DataSourceUIView, -} = require 'kraken/dataset/datasource-ui-view' - - - -/** - * @class - * Model is a Metric. - */ -MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ - __bind__ : <[ onChange ]> - tagName : 'section' - className : 'metric-edit-ui' - template : require 'kraken/template/metric-edit' - - callOnReturnKeypress : 'onChange' - events: - 'keydown .metric-label' : 'onReturnKeypress' - - graph_id : null - dataset : null - datasources : null - datasource_ui_view : null - - - constructor: function MetricEditView - BaseView ... - - initialize: -> - this import @options.{graph_id, dataset, datasources} - @model or= new Metric - BaseView::initialize ... - @on 'attach', @onAttach, this - @datasource_ui_view = new DataSourceUIView {@model, @graph_id, @dataset, @datasources} - @addSubview @datasource_ui_view - .on 'metric-update', ~> @trigger 'update', this - .on 'metric-change', @onSourceMetricChange, this - - - toTemplateLocals: -> - locals = MetricEditView.__super__.toTemplateLocals ... - locals import { - @graph_id, @dataset, @datasources - placeholder_label: @model.getPlaceholderLabel() - } - - update: -> - MetricEditView.__super__.update ... - @$ '.metric-label' .attr 'placeholder', @model.getPlaceholderLabel() - - # Update the color picker - @$ '.color-swatch' - .data 'color', @model.get 'color' - .colorpicker 'update' - - # @$ '.color-swatch' .css 'background-color', color - # @$ '.metric-color' .val color - this - - onAttach: -> - # console.log "#this.onAttach!" - @$ '.color-swatch' - .data 'color', @model.get 'color' - .colorpicker() - .on 'hide', @onChange - - onChange: (evt) -> - attrs = @$ 'form.metric-edit-form' .formData() - # attrs.color = that if evt?.color - same = _.isEqual @model.attributes, attrs - - console.log "#this.onChange! (same? #same)" - _.dump @model.attributes, 'old', not same - _.dump attrs, 'new', not same - # _.dump attrs, "#this.onChange! (same? #same)", not same - - unless _.isEqual @model.attributes, attrs - @model.set attrs, {+silent} - @trigger 'metric-update', this - - onSourceMetricChange: (metric) -> - console.log "#this.onSourceMetricChange!", metric - @$ '.metric-label' .attr 'placeholder', @model.getPlaceholderLabel() - @trigger 'metric-change', @model, this - this - -# }}} - diff --git a/lib/dataset/metric-model.co b/lib/dataset/metric-model.co deleted file mode 100644 index 2757b5b..0000000 --- a/lib/dataset/metric-model.co +++ /dev/null @@ -1,159 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseModel, BaseList, -} = require 'kraken/base' -DataSource = DataSourceList = null - -/** - * @class - */ -Metric = exports.Metric = BaseModel.extend do # {{{ - NEW_METRIC_LABEL : 'New Metric' - urlRoot : '/metrics' - - /** - * Data source of the Metric. - * @type DataSource - */ - source : null - - is_def_label : true - - defaults : -> - index : 0 - label : '' - type : 'int' - timespan : { start:null, end:null, step:null } - disabled : false - - # DataSource - source_id : null - source_col : -1 - - # Chart Options - color : null - visible : true - format_value : null - format_axis : null - - transforms : [] - scale : 1.0 - - - - constructor: function Metric - BaseModel ... - - initialize : -> - BaseModel::initialize ... - @is_def_label = @isDefaultLabel() - @on 'change:source_id', @load, this - @on 'change:source_col', @updateId, this - @on 'change:label', @updateLabel, this - @load() - - - getDateColumn: -> - @source.getDateColumn() - - getData: -> - @source.getColumn @get 'source_col' - - getLabel: -> - @get('label') or @getPlaceholderLabel() - - getPlaceholderLabel: -> - col = @get 'source_col' - name = "#{@source.get 'shortName'}, #{@source.getColumnName col}" if @source and col >= 0 - name or @NEW_METRIC_LABEL - - getSourceColumnName: -> - col = @get 'source_col' - @source.getColumnName col if @source and col > 0 - - - load: (opts={}) -> - source_id = @get 'source_id' - @resetReady() if opts.force or @source?.id is not source_id - return this if @loading or @ready - - unless source_id and @get('source_col') >= 0 - return @triggerReady() - - # console.log "#this.load()..." - @updateId() - @loading = true - @wait() - @trigger 'load', this - - DataSource.lookup source_id, (err, source) ~> - # console.log "#this.onSourceReady", arguments - @loading = false - @unwait() # terminates the `load` wait - if err - console.error "#{this} Error loading DataSource! #err" - else - # console.log "#{this}.load() complete!" - @source = source - @is_def_label = @isDefaultLabel() - @updateId() - @triggerReady() - this - - - isDefaultLabel: -> - label = @get 'label' - not label or label is @getSourceColumnName() or label is @NEW_METRIC_LABEL - - updateLabel: -> - return this unless @source - label = @get 'label' - if not label or @is_def_label - @set 'label', '' - @is_def_label = true - else - @is_def_label = @isDefaultLabel() - this - - updateId: -> - source_id = @get 'source_id' - source_col = @get 'source_col' - if source_id and source_col? - @id = "#source_id[#source_col]" - @updateLabel() - this - - - /** - * Check whether the metric has aiight-looking values so we don't - * attempt to graph unconfigured crap. - */ - isOk: -> - @source?.ready # and _.every @get('timespan'), op.ok - -# }}} - - -/** - * @class - */ -MetricList = exports.MetricList = BaseList.extend do # {{{ - urlRoot : '/metrics' - model : Metric - - constructor: function MetricList then BaseList ... - initialize: -> BaseList::initialize ... - - comparator: (metric) -> - metric.get('index') ? Infinity - - onlyOk: -> - new MetricList @filter -> it.isOk() - -# }}} - -### FIXME: LOLHACKS ### -setTimeout do - -> { DataSource, DataSourceList, } := require 'kraken/dataset/datasource-model' - 10 - diff --git a/lib/dataset/metric-view.co b/lib/dataset/metric-view.co deleted file mode 100644 index e69de29..0000000 diff --git a/lib/graph/graph-display-view.co b/lib/graph/graph-display-view.co index 3460149..b802f0d 100644 --- a/lib/graph/graph-display-view.co +++ b/lib/graph/graph-display-view.co @@ -17,7 +17,7 @@ root = do -> this GraphDisplayView = exports.GraphDisplayView = GraphView.extend do # {{{ tagName : 'section' className : 'graph graph-display' - template : require 'kraken/template/graph-display' + template : require 'kraken/template/graph/graph-display' events: # Select the whole permalink URI text when it receives focus. diff --git a/lib/graph/graph-edit-view.co b/lib/graph/graph-edit-view.co index aa260f5..12ef920 100644 --- a/lib/graph/graph-edit-view.co +++ b/lib/graph/graph-edit-view.co @@ -8,7 +8,7 @@ _ = require 'kraken/util/underscore' { ChartOptionScaffold, DEBOUNCE_RENDER, } = require 'kraken/chart' { DataView, DataSetView, DataSet, -} = require 'kraken/dataset' +} = require 'kraken/data' root = do -> this @@ -26,7 +26,7 @@ GraphEditView = exports.GraphEditView = GraphView.extend do # {{{ onFirstClickRenderOptionsTab onFirstClickRenderDataTab ]> className : 'graph-edit graph' - template : require 'kraken/template/graph-edit' + template : require 'kraken/template/graph/graph-edit' events: 'click .redraw-button' : 'stopAndRender' diff --git a/lib/graph/graph-list-view.co b/lib/graph/graph-list-view.co index 952845c..14bd007 100644 --- a/lib/graph/graph-list-view.co +++ b/lib/graph/graph-list-view.co @@ -19,7 +19,7 @@ GraphListView = exports.GraphListView = BaseView.extend do # {{{ tagName : 'section' className : 'graph-list-view' - template : require 'kraken/template/graph-list' + template : require 'kraken/template/graph/graph-list' data : {} ready : false diff --git a/lib/graph/graph-model.co b/lib/graph/graph-model.co index 0a2e02e..385ad7b 100644 --- a/lib/graph/graph-model.co +++ b/lib/graph/graph-model.co @@ -7,7 +7,7 @@ Seq = require 'seq' { ChartType, } = require 'kraken/chart' { DataSet -} = require 'kraken/dataset' +} = require 'kraken/data' root = do -> this diff --git a/lib/main-edit.co b/lib/main-edit.co index ee463f3..18e21cb 100644 --- a/lib/main-edit.co +++ b/lib/main-edit.co @@ -8,7 +8,7 @@ Backbone = require 'backbone' { ChartType, } = require 'kraken/chart' { DataSource, DataSourceList, -} = require 'kraken/dataset' +} = require 'kraken/data' { Graph, GraphList, GraphEditView, } = requir