From d19a142f1380083fa87a2f4a41426e99e4460c83 Mon Sep 17 00:00:00 2001 From: dsc Date: Tue, 24 Apr 2012 12:56:28 -0700 Subject: [PATCH] Moves timeseries classes. --- lib/base/base-view.co | 244 +++++++++++++++++++++++++------------ lib/base/index.co | 13 +- lib/chart/chart-option-model.co | 12 +- lib/chart/chart-option-view.co | 27 +++-- lib/dashboard/dashboard-view.co | 2 +- lib/dataset/data-view.co | 69 +++++++---- lib/dataset/dataset-view.co | 8 +- lib/dataset/datasource-model.co | 9 +- lib/dataset/datasource-ui-view.co | 9 ++- lib/dataset/metric-edit-view.co | 27 ++++- lib/dataset/metric-model.co | 4 +- lib/graph/graph-display-view.co | 14 +- lib/graph/graph-edit-view.co | 95 ++++++--------- lib/main-edit.co | 2 + lib/scaffold/scaffold-view.co | 51 ++++---- lib/template/chart-scaffold.jade | 3 +- lib/template/data.jade | 16 +-- lib/template/dataset.jade | 2 +- lib/template/datasource-ui.jade | 1 + lib/template/datasource.jade | 28 +++++ lib/template/graph-edit.jade | 6 +- lib/template/metric-edit.jade | 4 +- lib/timeseries/csv.co | 115 +++++++++++++++++ lib/timeseries/index.co | 2 + lib/timeseries/timeseries.co | 191 +++++++++++++++++++++++++++++ lib/util/index.co | 49 +++++--- lib/util/op.co | 32 +++-- lib/util/timeseries/csv.co | 118 ------------------ lib/util/timeseries/index.co | 2 - lib/util/timeseries/timeseries.co | 192 ----------------------------- 30 files changed, 750 insertions(+), 597 deletions(-) create mode 100644 lib/timeseries/csv.co create mode 100644 lib/timeseries/index.co create mode 100644 lib/timeseries/timeseries.co delete mode 100644 lib/util/timeseries/csv.co delete mode 100644 lib/util/timeseries/index.co delete mode 100644 lib/util/timeseries/timeseries.co diff --git a/lib/base/base-view.co b/lib/base/base-view.co index 7d22832..dce79ce 100644 --- a/lib/base/base-view.co +++ b/lib/base/base-view.co @@ -4,6 +4,8 @@ Backbone = require 'backbone' } = require 'kraken/util' { BaseBackboneMixin, mixinBase, } = require 'kraken/base/base-mixin' +{ DataBinding, +} = require 'kraken/base/data-binding' @@ -13,15 +15,20 @@ Backbone = require 'backbone' */ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ tagName : 'section' + model : BaseModel /** - * The identifier for this view class. - * By default, the class name is converted to underscore-case, and a - * trailing '_view' suffix is dropped. - * Example: "CamelCaseView" becomes "camel_case" + * Method-name called by `onReturnKeypress` when used as an event-handler. * @type String */ - __view_type_id__: null + callOnReturnKeypress: null + + + /** + * Parent view of this view. + * @type BaseView + */ + parent : null /** * Array of [view, selector]-pairs. @@ -33,7 +40,7 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ * Whether this view has been added to the DOM. * @type Boolean */ - _parented: false + isAttached: false @@ -42,7 +49,8 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ @__superclass__ = @..__super__.constructor @__view_type_id__ or= _.str.underscored @getClassName() .replace /_view$/, '' @waitingOn = 0 - @subviews = [] + @subviews = new ViewList + @onReturnKeypress = _.debounce @onReturnKeypress.bind(this), 50 Backbone.View ... @trigger 'create', this @@ -65,63 +73,130 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ @$el.data { @model, view:this } @model.on 'change', @render, this @model.on 'destroy', @remove, this - @model + @trigger 'change:model', this, model + model ### Subviews - addSubview: (selector, view) -> - [view, selector] = [selector, null] unless view - @subviews.push [view, selector] + setParent: (parent) -> + [old_parent, @parent] = [@parent, parent] + @trigger 'parent', this, parent, old_parent + this + + unsetParent: -> + [old_parent, @parent] = [@parent, null] + @trigger 'unparent', this, old_parent + this + + + addSubview: (view) -> + @removeSubview view + @subviews.push view + view.setParent this view removeSubview: (view) -> - for [v, sel], idx of @subviews - if v is view - @subviews.splice(idx, 1) - return [v, sel] - null + if @hasSubview view + view.remove() + @subviews.remove view + view.unsetParent() + view hasSubview: (view) -> - _.any @subviews, ([v]) -> v is view + @subviews.contains view invokeSubviews: -> - _ _.pluck(@subviews, 0) .invoke ...arguments + @subviews.invoke ...arguments - attachSubviews: -> - for [view, selector] of @subviews - return unless view - view.undelegateEvents() - return unless el = view.render()?.el - if selector - @$el.find selector .append el - else - @$el.append el - view.delegateEvents() + removeAllSubviews: -> + @subviews.forEach @removeSubview, this + @subviews = new ViewList this - removeAllSubviews: -> - @invokeSubviews 'remove' - _.pluck @subviews, 0 .forEach @removeSubview, this + + + ### UI Utilities + + attach: (el) -> + # @undelegateEvents() + @$el.appendTo el + # only trigger the event the first time + return this if @isAttached + @isAttached = true + _.delay do + ~> # have to let DOM settle to ensure elements can be found + @delegateEvents() + @trigger 'attach', this + 50 this - bubbleEvent: (evt) -> - @invokeSubviews 'trigger', ...arguments + remove : -> + # @undelegateEvents() + @$el.remove() + return this unless @isAttached + @isAttached = false + @trigger 'unattach', this + this + + clear : -> + @remove() + @model.destroy() + @trigger 'clear', this + this + + hide : -> @$el.hide(); @trigger('hide', this); this + show : -> @$el.show(); @trigger('show', this); this + + /** + * Attach each subview to its bind-point. + * @returns {this} + */ + attachSubviews: -> + bps = @getOwnSubviewBindPoints() + if @subviews.length and not bps.length + console.warn "#this.attachSubviews(): no subview bind-points found!" + return this + for view of @subviews + if bp = @findSubviewBindPoint view, bps + view.attach bp + else + console.warn "#this.attachSubviews(): Unable to find bind-point for #view!" this + /** + * Finds all subview bind-points under this view's element, but not under + * the view element of any subview. + * @returns {jQuery|undefined} + */ + getOwnSubviewBindPoints: -> + @$ '[data-subview]' .not @$ '[data-subview] [data-subview]' + + /** + * Find the matching subview bind-point for the given view. + */ + findSubviewBindPoint: (view, bind_points) -> + bind_points or= @getOwnSubviewBindPoints() + + # check if any bindpoint specifies this subview by id + if view.id + bp = bind_points.filter "[data-subview$=':#{view.id}']" + return bp.eq 0 if bp.length + + # Find all elements that specify this type as the subview type + bp = bind_points.filter "[data-subview='#{view.getClassName()}']" + return bp.eq 0 if bp.length + + ### Rendering Chain toTemplateLocals: -> - json = {value:v} = @model.toJSON() - if _.isArray(v) or _.isObject(v) - json.value = JSON.stringify v - json + @model.toJSON() - $template: (locals={}) -> - $ @template do - { $, _, op, @model, view:this } import @toTemplateLocals() import locals + $template: -> + $ @template { _, op, @model, view:this, ...@toTemplateLocals() } build: -> return this unless @template @@ -129,59 +204,52 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ @$el.html outer.html() .attr do id : outer.attr 'id' - class : outer.attr('class') + class : outer.attr 'class' @attachSubviews() + @isBuilt = true this render: -> - @build() + if @isBuilt + @update() + else + @build() + @renderSubviews() @trigger 'render', this this renderSubviews: -> - _.invoke _.pluck(@subviews, 0), 'render' + @attachSubviews() + @subviews.invoke 'render' this - - attach: (el) -> - @$el.appendTo el - # only trigger the event the first time - return this if @_parented - @_parented = true - _.delay do - ~> # have to let DOM settle to ensure elements can be found - @delegateEvents() - @trigger 'parent', this - 50 + update: -> + new DataBinding this .update @toTemplateLocals() + @trigger 'update', this this - remove : -> - @undelegateEvents() - @$el.remove() - return this unless @_parented - @_parented = false - @trigger 'unparent', this - this + /* * * * Events * * * */ - ### UI Utilities + bubbleEvent: (evt) -> + @invokeSubviews 'trigger', ...arguments + this - hide : -> @$el.hide(); @trigger('hide', this); this - show : -> @$el.show(); @trigger('show', this); this - clear : -> @model.destroy(); @trigger('clear', this); @remove() + redispatch: (evt) -> + @trigger ...arguments + this + onlyOnReturn: (fn, ...args) -> + fn = _.debounce fn.bind(this), 50 + (evt) ~> fn.apply this, args if evt.keyCode is 13 - # remove : -> - # if (p = @$el.parent()).length - # @$parent or= p - # # @parent_index = p.children().indexOf @$el - # @$el.remove() - # this - # - # reparent : (parent=@$parent) -> - # parent = $ parent - # @$el.appendTo parent if parent?.length - # this + /** + * Call a delegate on keypress == the return key. + * @returns {Function} Keypress event handler. + */ + onReturnKeypress: (evt) -> + fn = this[@callOnReturnKeypress] if @callOnReturnKeypress + fn.call this if fn and evt.keyCode is 13 toString : -> "#{@getClassName()}(model=#{@model})" @@ -194,3 +262,27 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ # }}} + + +class exports.ViewList extends Array + + (views=[]) -> + super ... + + extend: (views) -> + _.each views, ~> @push it + this + + findByModel: (model) -> + @find -> it.model is model + + toString: -> + contents = if @length then "\"#{@join '","'}\"" else '' + "ViewList[#{@length}](#contents)" + + +<[ each contains invoke pluck find remove compact flatten without union intersection difference unique uniq ]> + .forEach (methodname) -> + ViewList::[methodname] = -> _[methodname].call _, this, ...arguments + + diff --git a/lib/base/index.co b/lib/base/index.co index b3b17f8..9a31587 100644 --- a/lib/base/index.co +++ b/lib/base/index.co @@ -1,6 +1,7 @@ -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' -exports import mixins import models import views import cache import cascading +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' +exports import mixins import models import views import cache import cascading import data_binding diff --git a/lib/chart/chart-option-model.co b/lib/chart/chart-option-model.co index a531efe..cdba717 100644 --- a/lib/chart/chart-option-model.co +++ b/lib/chart/chart-option-model.co @@ -59,8 +59,8 @@ ChartOption = exports.ChartOption = Field.extend do # {{{ KNOWN_TAGS.update @getCategory() # Ignore functions/callbacks and, ahem, hidden tags. - type = @get 'type', '' .toLowerCase() - tags = @get 'tags', [] + type = @get 'type' .toLowerCase() or '' + tags = @get('tags') or [] if _.str.include(type, 'function') or _.intersection(tags, @IGNORED_TAGS).length @set 'ignore', true @@ -69,7 +69,7 @@ ChartOption = exports.ChartOption = Field.extend do # {{{ # will not trigger the 'changed:tags' event. addTag: (tag) -> return this unless tag - tags = @get('tags', []) + tags = @get('tags') or [] tags.push tag @set 'tags', tags this @@ -78,7 +78,7 @@ ChartOption = exports.ChartOption = Field.extend do # {{{ # will not trigger the 'changed:tags' event. removeTag: (tag) -> return this unless tag - tags = @get('tags', []) + tags = @get('tags') or [] _.remove tags, tag @set 'tags', tags this @@ -93,7 +93,7 @@ ChartOption = exports.ChartOption = Field.extend do # {{{ # A field's category is its first tag. getCategory: -> - @get('tags', [])[0] + tags = (@get('tags') or [])[0] getCategoryIndex: -> @getTagIndex @getCategory() @@ -112,7 +112,7 @@ ChartOption = exports.ChartOption = Field.extend do # {{{ * @class List of ChartOption fields. */ ChartOptionList = exports.ChartOptionList = FieldList.extend do # {{{ - model : ChartOption + model : ChartOption constructor: function ChartOptionList diff --git a/lib/chart/chart-option-view.co b/lib/chart/chart-option-view.co index bf7329a..86d86e0 100644 --- a/lib/chart/chart-option-view.co +++ b/lib/chart/chart-option-view.co @@ -19,9 +19,9 @@ ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{ isCollapsed : true events : - 'blur .value' : 'update' - 'click input[type="checkbox"].value' : 'update' - 'submit .value' : 'update' + 'blur .value' : 'change' + 'click input[type="checkbox"].value' : 'change' + 'submit .value' : 'change' 'click .close' : 'toggleCollapsed' 'click h3' : 'toggleCollapsed' 'click .collapsed' : 'onClick' @@ -32,6 +32,7 @@ ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{ render: -> FieldView::render ... + @$el.addClass 'ignore' if @get 'ignore' @$el.addClass 'collapsed' if @isCollapsed this @@ -106,9 +107,10 @@ ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{ render: -> console.log "#this.render(ready=#{@ready}) -> .isotope()" - # Scaffold::render ... + Scaffold::render ... return this unless @ready - container = if @fields then @$el.find @fields else @$el + + container = if @fields then @$ @fields else @$el container .addClass 'isotope' .find '.chart-option.field' .addClass 'isotope-item' @@ -125,21 +127,21 @@ ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{ this getOptionsFilter: -> - data = @$el.find '.options-filter-button.active' .toArray().map -> $ it .data() + data = @$ '.options-filter-button.active' .toArray().map -> $ it .data() sel = data.reduce do (sel, d) -> sel += that if d.filter sel - '' + ':not(.ignore)' sel collapseAll: -> - _.invoke @_subviews, 'collapse', true + _.invoke @subviews, 'collapse', true # @renderSubviews() false expandAll: -> - _.invoke @_subviews, 'collapse', false + _.invoke @subviews, 'collapse', false # @renderSubviews() false @@ -148,9 +150,10 @@ ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{ * Add a ChartOption to this scaffold, rerendering the isotope * layout after collapse events. */ - addOne: (field) -> - view = Scaffold::addOne ... - view.on 'change:collapse render', @render, this + addField: (field) -> + view = Scaffold::addField ... + # view.on 'change:collapse render', @render, this + view.on 'change:collapse', @render, this view toKV: -> diff --git a/lib/dashboard/dashboard-view.co b/lib/dashboard/dashboard-view.co index 470647b..fe66048 100644 --- a/lib/dashboard/dashboard-view.co +++ b/lib/dashboard/dashboard-view.co @@ -68,7 +68,7 @@ DashboardView = exports.DashboardView = BaseView.extend do # {{{ @trigger 'ready', this attachGraphs: -> - graphs_el = @$el.find '#graphs' + graphs_el = @$ '#graphs' for id of @graph_ids break unless graph = @graphs.get id continue if graph.view.isAttached diff --git a/lib/dataset/data-view.co b/lib/dataset/data-view.co index d031f97..f07549e 100644 --- a/lib/dataset/data-view.co +++ b/lib/dataset/data-view.co @@ -1,7 +1,7 @@ Seq = require 'seq' { _, op, } = require 'kraken/util' -{ BaseView, +{ BaseView, ViewList, } = require 'kraken/base' { DataSetView, } = require 'kraken/dataset/dataset-view' @@ -18,7 +18,6 @@ DataView = exports.DataView = BaseView.extend do # {{{ className : 'data-ui' template : require 'kraken/template/data' - data : {} datasources : null @@ -29,36 +28,41 @@ DataView = exports.DataView = BaseView.extend do # {{{ initialize: -> @graph_id = @options.graph_id BaseView::initialize ... + @metric_views = new ViewList @datasources = @model.sources + @model.metrics + .on 'add', @addMetric, this + .on 'remove', @removeMetric, this @on 'ready', @onReady, this @load() onReady: -> dataset = @model - @metric_edit_view = @addSubview new MetricEditView {@graph_id, dataset, @datasources} - @metric_edit_view - .on 'update', @onUpdateMetric, this + @model.metrics.each @addMetric, this + # @metric_edit_view = @addSubview new MetricEditView {@graph_id, dataset, @datasources} + # @metric_edit_view + # .on 'update', @onUpdateMetric, this - @dataset_view = @addSubview new DataSetView {@model, @graph_id, dataset, @datasources} - @dataset_view + @dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources} + @addSubview @dataset_view .on 'add-metric', @onMetricsChanged, this .on 'remove-metric', @onMetricsChanged, this .on 'edit-metric', @editMetric, this - @attachSubviews() + @render() this load: -> @wait() - $.getJSON '/datasources/all', (@data) ~> - _.each @data, @canonicalizeDataSource, this - @model.sources.reset _.map @data, -> it - @ready = true - @unwait() - @render() - @trigger 'ready', this + # $.getJSON '/datasources/all', (@data) ~> + # _.each @data, @canonicalizeDataSource, this + # @model.sources.reset _.map @data, -> it + @ready = true + @unwait() + # @render() + @trigger 'ready', this /** * Transform the `columns` field to ensure an Array of {label, type} objects. @@ -84,22 +88,33 @@ DataView = exports.DataView = BaseView.extend do # {{{ toTemplateLocals: -> attrs = _.clone @model.attributes - { $, _, op, @model, view:this, @graph_id, @datasources } import attrs + { @graph_id, @datasources } import attrs - # attachSubviews: -> - # @$el.empty() - # BaseView::attachSubviews ... - # @$el.append '
' + # Don't rebuild HTML, simply notify subviews + # render: -> + # @renderSubviews() + # @trigger 'render', this # this - # Don't rebuild HTML, simply notify subviews - render: -> - @renderSubviews() - @trigger 'render', this - this + addMetric: (metric) -> + console.log "#this.addMetric!", metric + return metric if @metric_views.findByModel metric + view = new MetricEditView {model:metric, @graph_id, dataset, @datasources} + @metric_views.push @addSubview view + metric + + removeMetric: (metric) -> + console.log "#this.removeMetric!", metric + return unless view = @metric_views.findByModel metric + @metric_views.remove view + @removeSubview view + metric editMetric: (metric) -> - @metric_edit_view.editMetric metric + console.log "#this.editMetric!", metric + @metric_views.invoke 'hide' + @metric_edit_view = @metric_views.findByModel metric + @metric_edit_view?.show() @onMetricsChanged() onMetricsChanged: -> @@ -107,7 +122,7 @@ DataView = exports.DataView = BaseView.extend do # {{{ newMinHeight = Math.max do oldMinHeight @dataset_view.$el.height() - @metric_edit_view.$el.height() + @metric_edit_view?.$el.height() # console.log 'onMetricsChanged!', oldMinHeight, '-->', newMinHeight @$el.css 'min-height', newMinHeight diff --git a/lib/dataset/dataset-view.co b/lib/dataset/dataset-view.co index efb7d0d..b201229 100644 --- a/lib/dataset/dataset-view.co +++ b/lib/dataset/dataset-view.co @@ -48,7 +48,7 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{ view = @addSubview new DataSetMetricView {model:metric, @graph_id} @views_by_cid[metric.cid] = view - @$el.find '.metrics' .append view.render().el + @$ '.metrics' .append view.render().el # @render() @trigger 'add-metric', metric, view, this @@ -68,13 +68,13 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{ this editMetric: (metric) -> - console.log "#this.editMetric!", metric + # console.log "#this.editMetric!", metric if metric instanceof [jQuery.Event, Event] metric = $ metric.currentTarget .data 'model' view = @active_view = @views_by_cid[metric.cid] - console.log ' --> metric:', metric, 'view:', view + console.log "#this.editMetric!", metric - @$el.find '.metrics .dataset-metric' .removeClass 'metric-active' + @$ '.metrics .dataset-metric' .removeClass 'metric-active' view.$el.addClass 'metric-active' view.$el.find '.activity-arrow' .css 'font-size', 2+view.$el.height() diff --git a/lib/dataset/datasource-model.co b/lib/dataset/datasource-model.co index b5b8674..cb3bfb4 100644 --- a/lib/dataset/datasource-model.co +++ b/lib/dataset/datasource-model.co @@ -1,5 +1,7 @@ -{ _, op, CSVData, +{ _, op, } = require 'kraken/util' +{ TimeSeriesData, CSVData, +} = require 'kraken/timeseries' { BaseModel, BaseList, BaseView, } = require 'kraken/base' { Metric, MetricList, @@ -59,7 +61,7 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{ @constructor.register this @metrics = new MetricList @attributes.metrics @on 'change:metrics', @onMetricChange, this - @load() + # @load() canonicalize: (ds) -> @@ -98,7 +100,7 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{ $.ajax do url : url dataType : 'json' - success : @onLoadSuccess + success : (data) ~> @onLoadSuccess new TimeSeriesData data error : @onLoadError this @@ -190,6 +192,7 @@ DataSource import do Cls = this @register new Cls {id} .on 'ready', -> cb.call cxt, null, it + .load() _.bindAll DataSource, 'register', 'get', 'lookup' diff --git a/lib/dataset/datasource-ui-view.co b/lib/dataset/datasource-ui-view.co index affc7d5..5765b41 100644 --- a/lib/dataset/datasource-ui-view.co +++ b/lib/dataset/datasource-ui-view.co @@ -14,7 +14,8 @@ DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{ template : require 'kraken/template/datasource-ui' events : - 'click .datasource-summary': 'onHeaderClick' + 'click .datasource-summary' : 'onHeaderClick' + 'click .datasource-source-metric' : 'onSelectMetric' graph_id : null dataset : null @@ -50,4 +51,10 @@ DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{ onHeaderClick: -> @$el.toggleClass 'in' + onSelectMetric: (evt) -> + tr = evt.currentTarget + idx = @$ '.source-metrics .datasource-source-metric' .toArray().indexOf tr + return unless idx is not -1 + + # }}} diff --git a/lib/dataset/metric-edit-view.co b/lib/dataset/metric-edit-view.co index b8f743b..47904f4 100644 --- a/lib/dataset/metric-edit-view.co +++ b/lib/dataset/metric-edit-view.co @@ -17,6 +17,10 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ className : 'metric-edit-ui' template : require 'kraken/template/metric-edit' + callOnReturnKeypress : 'onChanged' + events: + 'keydown .metric-label' : 'onReturnKeypress' + graph_id : null dataset : null datasources : null @@ -30,8 +34,10 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ this import @options.{graph_id, dataset, datasources} @model or= new Metric BaseView::initialize ... - @datasource_ui_view = @addSubview '.metric-datasource', new DataSourceUIView {@model, @graph_id, @dataset, @datasources} - @$el.find '.metric-datasource' .append @datasource_ui_view.render().el + @datasource_ui_view = new DataSourceUIView {@model, @graph_id, @dataset, @datasources} + @addSubview @datasource_ui_view + .on 'update', ~> @trigger 'update', this + @$ '.metric-datasource' .append @datasource_ui_view.render().el toTemplateLocals: -> @@ -41,12 +47,25 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ build: -> BaseView::build ... if @datasource_ui_view - @$el.find '.metric-datasource' .append @datasource_ui_view.render().el + @$ '.metric-datasource' .append @datasource_ui_view.render().el + this + + update: -> + color = @model.get 'color' + @$ '.color-swatch' .css 'background-color', color + @$ '.metric-color' .val color + @$ '.metric-label' .val @model.get 'label' + this + onChanged: -> + attrs = @$ 'form.metric-edit-form' .formData() + @model.set attrs, {+silent} + @trigger 'update', this + editMetric: (metric) -> console.log "#this.editMetric!", metric - @datasource_ui_view.model = @model = metric + @datasource_ui_view.setModel @setModel metric @render() @show() this diff --git a/lib/dataset/metric-model.co b/lib/dataset/metric-model.co index d339283..ffef7a9 100644 --- a/lib/dataset/metric-model.co +++ b/lib/dataset/metric-model.co @@ -49,8 +49,8 @@ Metric = exports.Metric = BaseModel.extend do # {{{ @lookupSource() - getDates: -> - @source.getDates() + getDateColumn: -> + @source.getDateColumn() getData: -> @source.getColumn @get 'source_col' diff --git a/lib/graph/graph-display-view.co b/lib/graph/graph-display-view.co index c4da4d8..e698d7b 100644 --- a/lib/graph/graph-display-view.co +++ b/lib/graph/graph-display-view.co @@ -123,9 +123,9 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ return { width, height } unless @ready # Remove old style, as it confuses dygraph after options update - viewport = @$el.find '.viewport' + viewport = @$ '.viewport' viewport.attr 'style', '' - label = @$el.find '.graph-legend' + label = @$ '.graph-legend' if width is 'auto' vpWidth = viewport.innerWidth() @@ -147,12 +147,12 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ renderChart: -> data = @model.get 'dataset' #.getData() size = @resizeViewport() - viewport = @$el.find '.viewport' + viewport = @$ '.viewport' # XXX: use @model.changedAttributes() to calculate what to update options = @chartOptions() #import size options import do - labelsDiv : @$el.find '.graph-legend' .0 + labelsDiv : @$ '.graph-legend' .0 valueFormatter : @numberFormatterHTML axes: x: @@ -186,8 +186,8 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ update: -> locals = @toTemplateLocals() - @$el.find '.graph-name a' .text(locals.name or '') - @$el.find '.graph-desc' .html jade.filters.markdown locals.desc or '' + @$ '.graph-name a' .text(locals.name or '') + @$ '.graph-desc' .html jade.filters.markdown locals.desc or '' this render: -> @@ -268,7 +268,7 @@ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ # last action that happens. If we don't # defer, the focusing click will # unselect the text. - _.defer( ~> @$el.find '.graph-permalink input' .select() ) + _.defer( ~> @$ '.graph-permalink input' .select() ) # Needed because (sigh) _.debounce returns undefined stopAndRender: -> diff --git a/lib/graph/graph-edit-view.co b/lib/graph/graph-edit-view.co index 30edab8..e7d403f 100644 --- a/lib/graph/graph-edit-view.co +++ b/lib/graph/graph-edit-view.co @@ -27,12 +27,12 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ valueFormatter xValueFormatter yValueFormatter ]> __bind__ : <[ - render renderAll stopAndRender stopAndRenderAll resizeViewport wait unwait checkWaiting + render stopAndRender resizeViewport wait unwait checkWaiting numberFormatter numberFormatterHTML onReady onSync onModelChange onScaffoldChange onFirstClickRenderOptionsTab onFirstClickRenderDataTab ]> - __debounce__: <[ render renderAll ]> + __debounce__: <[ render ]> tagName : 'section' className : 'graph-edit graph' template : require 'kraken/template/graph-edit' @@ -68,7 +68,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ BaseView ... initialize : (o={}) -> - # @data = {} @model or= new Graph @id = @graph_id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid) BaseView::initialize ... @@ -98,23 +97,23 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ .on 'ready', @onReady, this ### Chart Options Tab, Scaffold - @scaffold = @addSubview '.graph-options-pane', new ChartOptionScaffold - @$el.find '.graph-options-pane' .append @scaffold.el + @scaffold = @addSubview new ChartOptionScaffold + # @$ '.graph-options-pane' .append @scaffold.el @scaffold.collection.reset that if o.graph_spec @scaffold.on 'change', @onScaffoldChange @chartOptions @model.getOptions(), {+silent} - @$el.on 'click', '.graph-data-tab', @onFirstClickRenderDataTab - # Rerender the options boxes once the tab is visible + # Rerender once the tab is visible # Can't use @events because we need to bind before registering - @$el.on 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab + @$el.on 'click', '.graph-data-tab', @onFirstClickRenderDataTab + @$el.on 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab ### Graph Data UI - @data = @addSubview '.graph-data-pane', new DataView { model:@model.get('data'), graph_id:@id } - @$el.find '.graph-data-pane' .append @data.render().el - @data + @data_view = @addSubview new DataView { model:@model.get('data'), graph_id:@id } + # @$ '.graph-data-pane' .append @data_view.render().el + @data_view .on 'change', @onDataChange, this .on 'start-waiting', @wait, this .on 'stop-waiting', @unwait, this @@ -180,9 +179,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ toTemplateLocals: -> attrs = _.clone @model.attributes delete attrs.options - # delete attrs.dataset - # attrs.data = @data - { $, _, op, @model, view:this, @graph_id, slug:'', name:'', desc:'' } import attrs + { @model, view:this, @graph_id, slug:'', name:'', desc:'' } import attrs /** @@ -194,11 +191,11 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ modelH = height = @model.get 'height' return { width, height } unless @ready - viewport = @$el.find '.viewport' + viewport = @$ '.viewport' # Remove old style, as it confuses dygraph after options update viewport.attr 'style', '' - label = @$el.find '.graph-label' + label = @$ '.graph-label' if width is 'auto' vpWidth = viewport.innerWidth() @@ -218,7 +215,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ # Repopulate UI from Model renderDetails: -> - form = @$el.find 'form.graph-details' + form = @$ 'form.graph-details' for k, v in @model.attributes continue if k is 'options' txt = @model.serialize v @@ -232,7 +229,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ form.find "textarea[name=#k]" .text txt # Graph Name field is not part of the form due to the layout. - @$el.find "input.graph-name[name='name']" .val @get 'name' + @$ "input.graph-name[name='name']" .val @get 'name' this # Redraw chart inside viewport. @@ -247,7 +244,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ options = @chartOptions() #import size options import do labels : dataset.getLabels() - labelsDiv : @$el.find '.graph-label' .0 + labelsDiv : @$ '.graph-label' .0 valueFormatter : @numberFormatterHTML axes: x: @@ -264,13 +261,13 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ # dygraphs to reset the current option state. @chart?.destroy() @chart = new Dygraph do - @$el.find '.viewport' .0 + @$ '.viewport' .0 data options # unless @chart # @chart = new Dygraph do - # @$el.find '.viewport' .0 + # @$ '.viewport' .0 # data # options # else @@ -280,33 +277,23 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ this attachSubviews: -> - @$el.find '.graph-options-pane' .append @scaffold.el if @scaffold - @$el.find '.graph-data-pane' .append @data.render().el if @data + BaseView::attachSubviews ... @checkWaiting() - render: -> - return this unless @ready and not @_rendering - @_rendering = true - @wait() - @checkWaiting() - @renderDetails() - @attachSubviews() - # _.invoke @subviews, 'render' - @renderChart() - @updateURL() - @trigger 'render', this - @unwait() - @_rendering = false - this - - renderAll: -> - return this unless @ready - # console.log "#this.renderAll!" - @wait() - _.invoke @scaffold.subviews, 'render' - @scaffold.render() - @render() - @unwait() + # render: -> + # return this unless @ready and not @_rendering + # @_rendering = true + # @wait() + # @checkWaiting() + # @renderDetails() + # @attachSubviews() + # # _.invoke @subviews, 'render' + # @renderChart() + # @updateURL() + # @trigger 'render', this + # @unwait() + # @_rendering = false + # this /** @@ -324,7 +311,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ * Retrieve or construct the spinner. */ spinner: -> - el = @$el.find '.graph-spinner' + el = @$ '.graph-spinner' unless el.data 'spinner' ### Spin.js Options ### opts = @@ -407,7 +394,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ onReady: -> return if @ready - # $.getJSON '/datasources/all', (@data) ~> console.log "(#this via GraphEditView).ready!" @ready = @scaffold.ready = true @unwait() # clears `wait()` from `initialize` @@ -420,10 +406,8 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ return unless @ready console.info "#this.sync() --> success!" # TODO: UI alert - # @change() - # @model.change() @chartOptions @model.getOptions(), {+silent} - @renderAll() + @render() onStartWaiting: -> console.log "#this.onStartWaiting!", @checkWaiting() @@ -472,17 +456,15 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ @scaffold.render() onFirstClickRenderDataTab: -> - # @$el.off 'click', '.graph-data-tab', @onFirstClickRenderDataTab - _.defer ~> @data.onMetricsChanged() + @$el.off 'click', '.graph-data-tab', @onFirstClickRenderDataTab + _.defer ~> @data_view.onMetricsChanged() onKeypress: (evt) -> $(evt.target).submit() if evt.keyCode is 13 onDetailsSubmit: -> console.log "#this.onDetailsSubmit!" - data = _.synthesize do - @$el.find('form.graph-details').serializeArray() - -> [it.name, it.value] + data = @$ 'form.graph-details' .formData() @model.set data false @@ -493,7 +475,6 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ # Needed because (sigh) _.debounce returns undefined, and we need to preventDefault() stopAndRender : -> @render ... ; false - stopAndRenderAll : -> @renderAll ... ; false # }}} diff --git a/lib/main-edit.co b/lib/main-edit.co index 2bd6679..d020caa 100644 --- a/lib/main-edit.co +++ b/lib/main-edit.co @@ -3,6 +3,8 @@ Backbone = require 'backbone' { _, op, } = require 'kraken/util' +{ TimeSeriesData, CSVData, +} = require 'kraken/timeseries' { BaseView, BaseModel, BaseList, } = require 'kraken/base' { Field, FieldList, FieldView, Scaffold, diff --git a/lib/scaffold/scaffold-view.co b/lib/scaffold/scaffold-view.co index 1b74bdb..5690062 100644 --- a/lib/scaffold/scaffold-view.co +++ b/lib/scaffold/scaffold-view.co @@ -13,8 +13,8 @@ FieldView = exports.FieldView = BaseView.extend do # {{{ type : 'string' events : - 'blur .value' : 'update' - 'submit .value' : 'update' + 'blur .value' : 'change' + 'submit .value' : 'change' constructor: function FieldView @@ -23,19 +23,25 @@ FieldView = exports.FieldView = BaseView.extend do # {{{ initialize: -> # console.log "#this.initialize!" BaseView::initialize ... - @type = @model.get('type', 'string').toLowerCase() + @type = @model.get('type').toLowerCase() or 'string' - update: -> + toTemplateLocals: -> + json = {value:v} = @model.toJSON() + if _.isArray(v) or _.isPlainObject(v) + json.value = JSON.stringify v + json + + change: -> if @type is 'boolean' - val = !! @$el.find('.value').attr('checked') + val = !! @$('.value').attr('checked') else - val = @model.getParser() @$el.find('.value').val() + val = @model.getParser() @$('.value').val() current = @model.getValue() return if _.isEqual val, current - console.log "#this.update( #current -> #val )" + console.log "#this.change( #current -> #val )" @model.setValue val, {+silent} - @trigger 'update', this + @trigger 'change', this render: -> return @remove() if @model.get 'ignore', false @@ -61,7 +67,7 @@ FieldView = exports.FieldView = BaseView.extend do # {{{ # model, collection, el, id, className, tagName, attributes Scaffold = exports.Scaffold = BaseView.extend do # {{{ - __bind__ : <[ addOne addAll ]> + __bind__ : <[ addField resetFields ]> tagName : 'form' className : 'scaffold' @@ -78,19 +84,14 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{ @model = (@collection or= new CollectionType) BaseView::initialize ... - @collection.on 'add', @addOne - @collection.on 'reset', @addAll + @collection.on 'add', @addField, this + @collection.on 'reset', @resetFields, this @$el.data { model:@collection, view:this } .addClass @className - renderSubviews: -> - _.invoke @_subviews, 'render' - this - - addOne: (field) -> - # console.log "[S] #this.addOne!", @..__super__ - _.remove @_subviews, field.view if field.view + addField: (field) -> + @removeSubview field.view if field.view # avoid duplicating event propagation field.off 'change:value', @change, this @@ -99,19 +100,15 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{ field.on 'change:value', @change, this SubviewType = @subviewType - view = new SubviewType model:field - @_subviews.push view - container = if @fields then @$el.find @fields else @$el - container.append view.render().el unless field.get 'ignore' - view.on 'update', @change.bind(this, field) + view = @addSubview new SubviewType model:field + view.on 'change', @change.bind(this, field) @render() view - addAll: -> - _.invoke @_subviews, 'remove' - @_subviews = [] - @collection.each @addOne + resetFields: -> + @removeAllSubviews() + @collection.each @addField this change: (field) -> diff --git a/lib/template/chart-scaffold.jade b/lib/template/chart-scaffold.jade index 565cd6b..7b7c895 100644 --- a/lib/template/chart-scaffold.jade +++ b/lib/template/chart-scaffold.jade @@ -9,6 +9,5 @@ form.chart-options.scaffold.form-inline a.advanced-filter-button.options-filter-button.btn(href="#", data-filter="") Advanced - .fields - //- .fields.control-group + .fields(data-subview="ChartOptionView") diff --git a/lib/template/data.jade b/lib/template/data.jade index e51e63b..cd827c9 100644 --- a/lib/template/data.jade +++ b/lib/template/data.jade @@ -1,16 +1,4 @@ section.data-ui - //- - .row-fluid - //- - .dataset-controls.dropdown - select(name="dataset") - for datum, k in datasources - - var selected = (datum.url === dataset ? 'selected' : null); - option(value=datum.url, name=datum.id, selected=selected) #{datum.name} - - //- - a.span3.dataset.dropdown-toggle(data-toggle="dropdown", data-target="#{graph_id} .dataset-dropdown", href="#dataset-dropdown") Select... - ul.dropdown-menu - for datum, k in datasources - li: a(href="#", data-dataset="#{datum.id}") #{datum.name} + .metric_edit_view_pane(data-subview="MetricEditView") + .data_set_view_pane(data-subview="DataSetView") diff --git a/lib/template/dataset.jade b/lib/template/dataset.jade index 1eb4e62..289d571 100644 --- a/lib/template/dataset.jade +++ b/lib/template/dataset.jade @@ -18,7 +18,7 @@ section.dataset-ui.dataset: div.inner th.col-source Source th.col-times Timespan th.col-actions Actions - tbody.metrics + tbody.metrics(data-subview="DataSetMetricView") //- DataSetMetricViews attach here diff --git a/lib/template/datasource-ui.jade b/lib/template/datasource-ui.jade index 193b4eb..13c2ec5 100644 --- a/lib/template/datasource-ui.jade +++ b/lib/template/datasource-ui.jade @@ -23,6 +23,7 @@ section.datasource-ui li(class=activeClass): a(href="#datasource-selector_datasource-source-#{ds.id}", data-toggle="tab", data-target=ds_target) #{ds.shortName} .datasource-sources-info.tab-content + //- DataSourceViews attach here for source, k in datasources.models - var ds = source.attributes - var activeClass = (source_id === ds.id ? 'active' : '') diff --git a/lib/template/datasource.jade b/lib/template/datasource.jade index e69de29..eec762a 100644 --- a/lib/template/datasource.jade +++ b/lib/template/datasource.jade @@ -0,0 +1,28 @@ +- var ds = source.attributes +- var activeClass = (source_id === ds.id ? 'active' : '') +.datasource-source.tab-pane(class="datasource-source-#{ds.id} #{activeClass}") + .datasource-source-details.well + .source-name #{ds.name} + .source-id #{ds.id} + .source-format #{ds.format} + .source-charttype #{ds.chart.chartType} + input.source-url(type="text", name="source-url", value=ds.url) + .datasource-source-time + .source-time-start #{ds.timespan.start} + .source-time-end #{ds.timespan.end} + .source-time-step #{ds.timespan.step} + .datasource-source-metrics + table.table.table-striped + thead + tr + th.source-metric-idx # + th.source-metric-label Label + th.source-metric-type Type + tbody.source-metrics + for m, idx in ds.metrics.slice(1) + - var activeColClass = (activeClass && source_col === m.idx) ? 'active' : '' + tr.datasource-source-metric(class=activeColClass) + td.source-metric-idx #{m.idx} + td.source-metric-label #{m.label} + td.source-metric-type #{m.type} + diff --git a/lib/template/graph-edit.jade b/lib/template/graph-edit.jade index a8824d4..83179c1 100644 --- a/lib/template/graph-edit.jade +++ b/lib/template/graph-edit.jade @@ -1,9 +1,7 @@ - var graph_id = view.id || model.id || model.cid section.graph-edit.graph(id=graph_id) - //- Graph Name field is not part of the form due to the layout. .graph-name-row.graph-details.row-fluid.control-group - //- label.name.control-label(for="#{id}_name"): h3 Graph Name input.span6.graph-name(type='text', id="#{graph_id}_name", name="name", placeholder='Graph Name', value=name) .graph-viewport-row.row-fluid @@ -66,12 +64,12 @@ section.graph-edit.graph(id=graph_id) p.help-block A description of the graph. - .graph-data-pane.tab-pane(id="#{graph_id}-tab-data") + .graph-data-pane.tab-pane(id="#{graph_id}-tab-data", data-subview="DataView") //- .row-fluid label.dataset.control-label(for="#{graph_id}_dataset") Data Set input.span3.dataset(type='text', id="#{graph_id}_dataset", name='dataset', placeholder='URL to dataset file', value=dataset) p.help-block This dataset filename will soon be replaced by a friendly UI. - .graph-options-pane.tab-pane(id="#{graph_id}-tab-options") + .graph-options-pane.tab-pane(id="#{graph_id}-tab-options", data-subview="ChartOptionScaffold") diff --git a/lib/template/metric-edit.jade b/lib/template/metric-edit.jade index ffe0a7f..d8c37cc 100644 --- a/lib/template/metric-edit.jade +++ b/lib/template/metric-edit.jade @@ -1,12 +1,12 @@ section.metric-edit-ui - .inner: form.form-horizontal + .inner: form.metric-edit-form.form-horizontal .metric-header.control-group .color-swatch(style="background-color: #{color};") input.metric-color(type='hidden', id="#{graph_id}_metric", name="color", value=color) input.metric-label(type='text', id="#{graph_id}_metric_label", name='label', placeholder='Metric Label', value=label) - .metric-datasource.control-group + .metric-datasource.control-group(data-subview="DataSourceUIView") .metric-actions.control-group a.delete-button.btn.btn-danger(href="#") diff --git a/lib/timeseries/csv.co b/lib/timeseries/csv.co new file mode 100644 index 0000000..ec87318 --- /dev/null +++ b/lib/timeseries/csv.co @@ -0,0 +1,115 @@ +_ = require 'kraken/util/underscore' +TimeSeriesData = require 'kraken/timeseries/timeseries' + + +DASH_PATTERN = /-/g +BLANK_LINE_PATTERN = /^(\s*)$/ +COMMENT_PATTERN = /\s*(#|\/\/).*$/ + +class CSVData extends TimeSeriesData + DEFAULT_OPTIONS : + colSep : ',' + rowSep : '\n' + defaultType : 'float' + customBars : false + customSep : ';' + errorBars : false + fractions : false + fractionSep : '/' + skipBlankLines : true + blankLinePat : BLANK_LINE_PATTERN + removeCommentedText : true + commentPat : COMMENT_PATTERN + replaceMissing : false + replaceMissingValue : 0 + replaceNaN : false + replaceNaNValue : 0 + padRows : false + padRowsValue : 0 + + + (data, opts) -> + super ... + + + /* * * * CSV Parsing * * * */ + + parseNumber: (s) -> + parseFloat s + + parseHiLo: (s) -> + s.split @options.customBars .map @parseNumber, this + + parseFraction: (s) -> + s.split @options.fractionSep .map @parseNumber, this + + parseDate: (s) -> + new Date s.replace DASH_PATTERN, '/' + + + /** + * Parses and imports a CSV string. + * + * @private + * @returns {this} + */ + parse: (@rawData) -> + return this if typeof rawData is not 'string' + o = @options + + lines = rawData.split o.rowSep + return [] unless lines.length + first = lines[0] + + # Use the default delimiter or fall back to a tab if that makes sense. + delim = o.colSep + if first.indexOf(delim) is -1 and first.indexOf('\t') >= 0 + delim = '\t' + + rows = @rows = [] + @columns = [] + + parser = @parseNumber + parser = @parseHiLo if o.customBars + parser = @parseFraction if o.fractions + + hasHeaders = @labels.length is not 0 + for line, i of lines + line .= replace o.commentPat, '' if o.removeCommentedText + continue if o.skipBlankLines and (line.length is 0 or o.blankLinePat.test line) + + cols = line.split delim + unless hasHeaders + hasHeaders = true + @labels = cols.map -> _.strip it + continue + + continue unless cols.length > 1 + date = @parseDate cols.shift() + fields = cols.map parser, this + if o.errorBars + fields = fields.reduce do + (acc, v) -> + last = acc[acc.length-1] + unless last and last.length < 2 + acc.push last = [] + last.push v + acc + [] + + fields.unshift date + rows.push fields + fields.forEach (v, idx) ~> + @columns.push [] unless @columns[idx] + @columns[idx].push v + + @untransformedRows = _.merge [], @rows + this + + + + + +module.exports = exports = CSVData + + diff --git a/lib/timeseries/index.co b/lib/timeseries/index.co new file mode 100644 index 0000000..6fa58c2 --- /dev/null +++ b/lib/timeseries/index.co @@ -0,0 +1,2 @@ +exports.TimeSeriesData = require 'kraken/timeseries/timeseries' +exports.CSVData = require 'kraken/timeseries/csv' diff --git a/lib/timeseries/timeseries.co b/lib/timeseries/timeseries.co new file mode 100644 index 0000000..f9ff9cb --- /dev/null +++ b/lib/timeseries/timeseries.co @@ -0,0 +1,191 @@ +_ = require 'kraken/util/underscore' + + + +/** + * @class Represents a collection of data columns aligned along a common timeline. + */ +class TimeSeriesData + DEFAULT_OPTIONS : {} + + options : {} + labels : [] + types : [] + + untransformedRows : null # row-oriented (untransformed) + rows : null # row-oriented + columns : null # column-oriented (includes date column) + dateColumn : null # only date column + dataColumns : null # column-oriented (excludes date column) + + + /** + * @constructor + */ + (data, opts) -> + unless typeof data is 'string' or _.isArray data + [opts, data] = [data, null] + @options = _.clone(@DEFAULT_OPTIONS) import (opts or {}) + + @transforms = [] + @labels = @options.labels or [] + @types = @options.types or [] + + @parse that if data or @options.data + @rebuildDerived() + + + /* * * * TimeSeriesData interface * * * */ + + + /** + * @returns {Array} List of rows, each of which includes all columns. + */ + getData: -> + @data + + /** + * @returns {Array} List of all columns (including date column). + */ + getColumns: -> + @columns + + /** + * @returns {Array} The date column. + */ + getDateColumn: -> + @dateColumn + + /** + * @returns {Array} List of all columns except the date column. + */ + getDataColumns: -> + @dataColumns + + /** + * @returns {Array} List of column labels. + */ + getLabels: -> + @labels + + + /* * * * Parsing * * * */ + + /** + * Subclass and override to perform preprocessing of the data. + * @private + */ + parse : (rawData) -> + this + + /** + * Rebuilds the row-oriented data matrix from the columns. + * @private + */ + rebuildData: -> + @rows = _.zip ...@columns + @rebuildDerived() + + /** + * Rebuilds the column-oriented data matrix from the columns. + * @private + */ + rebuildColumns: -> + @columns = _.zip ...@rows + @rebuildDerived() + + /** + * @private + */ + rebuildDerived: -> + while @transforms.length < @columns.length + @transforms.push [] + @dateColumn = @columns[0] + @dataColumns = @columns.slice(1) + this + + + + /* * * * Data Transformation * * * */ + + /** + * Applies the stack of transforms to the data. + * + * TODO: Apply transforms in @getData()? + * @private + * @returns {this} + */ + applyTransforms: -> + for fns, idx of @transforms + for fn of fns + @columns[idx] .= map fn, ctx + @rebuildData() + + /** + * Clears all transforms and restores the original data. + * @returns {this} + */ + clearTransforms: -> + @transforms = [] + @rows = _.merge [], @untransformedRows + @rebuildColumns() + + /** + * Add a data transform to the specified columns. The function is + * applied one-by-one (in column-major order), replacing the data + * with the mapped result. + * + * @param {Number|Array} indices List one or more column indices to map. Negative + * numbers are offset from the end of the columns list. + * @param {Function} fn Mapping function of the form: + * `(single_value, row_idx, column) -> new_value` + * @param {Object} [ctx=this] Execution context for the function. + * @returns {this} + */ + addTransform: (indices, fn, ctx=this) -> + num_cols = @columns.length + if typeof idx is 'function' + [ctx, fn, indices] = [fn, indices, null] + unless indices? + indices = _.range num_cols + unless _.isArray indices + indices = [indices] + for idx of indices + idx %= num_cols + idx += num_cols if idx < 0 + @transforms[idx].push fn + @applyTransforms() + + /** + * Add a data transform to all columns except the date column. The function + * is applied one-by-one (in column-major order), replacing the data + * with the mapped result. + * + * @param {Function} fn Mapping function of the form: + * `(single_value, row_idx, column) -> new_value` + * @param {Object} [ctx=this] Execution context for the function. + * @returns {this} + */ + addDataTransform: (fn, ctx=this) -> + @addTransform _.range(1, @columns.length), fn, ctx + + + + /* * * * Misc * * * */ + + /** + * @returns {Array} Deep copy of the data rows (including all columns). + */ + toJSON: -> + _.merge [], @getData() + + toString: -> + labels = @labels + .map -> "'#it'" + .join ', ' + "#{@..name or @..displayName}(#labels)" + + + +module.exports = exports = TimeSeriesData + diff --git a/lib/util/index.co b/lib/util/index.co index e98d502..1b5c01e 100644 --- a/lib/util/index.co +++ b/lib/util/index.co @@ -1,21 +1,36 @@ -_ = require 'kraken/util/underscore' +_ = exports._ = require 'kraken/util/underscore' +op = exports.op = require 'kraken/util/op' -root = do -> this -root.console or= _ <[ log info warn error dir table group groupCollapsed groupEnd ]> .synthesize -> [it, nop] +# Root object -- `window` in the browser, `global` in Node. +root = exports.root = do -> this +# Stub out console with empty methods +root.console or= _ <[ log info warn error dir table group groupCollapsed groupEnd ]> .synthesize -> [it, op.nop] + +### Extend jQuery with useful functions + +/** + * @returns {Object} Object of the data from the form, via `.serializeArray()`. + */ +root.jQuery?.fn.formData = -> + _.synthesize do + this.serializeArray() + -> [it.name, it.value] + +/** + * Invokes a jQuery method on each element, returning the array of the result. + * @returns {Array} Results. + */ root.jQuery?.fn.invoke = (method, ...args) -> for el, idx of this - el = jQuery(el) - el[method] ...args - -op = require 'kraken/util/op' -backbone = require 'kraken/util/backbone' -parser = require 'kraken/util/parser' -Cascade = require 'kraken/util/cascade' -CSVData = require 'kraken/util/csv' -exports import { root, _, op, backbone, parser, Cascade, CSVData, } - -# HashSet = require 'kraken/util/hashset' -# BitString = require 'kraken/util/bitstring' -# {crc32} = require 'kraken/util/crc' -# exports import { HashSet, BitString, crc32, } + jQuery(el)[method] ...args + + +backbone = exports.backbone = require 'kraken/util/backbone' +parser = exports.parser = require 'kraken/util/parser' +Cascade = exports.Cascade = require 'kraken/util/cascade' + +# HashSet = exports.HashSet = require 'kraken/util/hashset' +# BitString = exports.BitString = require 'kraken/util/bitstring' +# {crc32} = exports.{crc32} = require 'kraken/util/crc' + diff --git a/lib/util/op.co b/lib/util/op.co index 9129433..83477f0 100644 --- a/lib/util/op.co +++ b/lib/util/op.co @@ -1,11 +1,15 @@ +STRIP_PAT = /(^\s*|\s*$)/g +strip = (s) -> + if s then s.replace STRIP_PAT, '' else s FALSEY = /^\s*(?:no|off|false)\s*$/i parseBool = (s) -> i = parseInt(s or 0) !! if isNaN(i) then not FALSEY.test(s) else i + + module.exports = op = - I : (x) -> x K : (k) -> -> k nop : -> @@ -13,7 +17,7 @@ module.exports = op = kObject : -> {} kArray : -> [] - # values + ### values val : (def,o) -> o ? def ok : (o) -> o? notOk : (o) -> o!? @@ -32,14 +36,14 @@ module.exports = op = arguments[1] = a fn.apply this, arguments - # reduce-ordered values & accessors + ### reduce-ordered values & accessors khas : (k,o) -> k in o kget : (k,o) -> o[k] defkget : (def,k,o) -> if k in o then o[k] else def thisget : (k) -> this[k] vkset : (o,v,k) -> o[k] = v if o and k?; o - # curry-ordered values & accessors + ### curry-ordered values & accessors has : (o,k) -> k in o get : (o,k) -> o[k] getdef : (o,k,def) -> if k in o then o[k] else def @@ -52,15 +56,19 @@ module.exports = op = obj[name] ...args.concat(_args) if obj?[name] isK : (k) -> (v) -> v is k - # type coercion (w/ limited parameters for mapping) + ### type coercion (w/ limited parameters for mapping) parseBool : parseBool toBool : parseBool toInt : (v) -> parseInt v toFloat : (v) -> parseFloat v toStr : (v) -> String v - toObject : (v) -> if typeof v is 'string' then JSON.parse(v) else v + toObject : (v) -> + if typeof v is 'string' and strip(v) + JSON.parse v + else + v - # comparison + ### comparison cmp : (x,y) -> if x < y then -1 else (if x > y then 1 else 0) eq : (x,y) -> x == y ne : (x,y) -> x != y @@ -69,7 +77,7 @@ module.exports = op = lt : (x,y) -> x < y le : (x,y) -> x <= y - # math + ### math add : (x,y) -> x + y sub : (x,y) -> x - y mul : (x,y) -> x * y @@ -80,14 +88,14 @@ module.exports = op = log2 : (n) -> Math.log n / Math.LN2 - # logic + ### logic is : (x,y) -> x is y isnt : (x,y) -> x is not y and : (x,y) -> x and y or : (x,y) -> x or y not : (x) -> not x - # bitwise + ### bitwise bitnot : (x) -> ~x bitand : (x,y) -> x & y bitor : (x,y) -> x | y @@ -96,7 +104,7 @@ module.exports = op = rshift : (x,y) -> x >> y # zrshift : (x,y) -> x >>> y - # binary + ### binary # Binary representation of the number. bin : (n) -> @@ -119,5 +127,5 @@ module.exports = op = ord : -> String(it).charCodeAt 0 encode : -> it and $ "
#it
" .html().replace /"/g, '"' decode : -> it and $ "
#it
" .text() - + strip : strip diff --git a/lib/util/timeseries/csv.co b/lib/util/timeseries/csv.co deleted file mode 100644 index 8e9409e..0000000 --- a/lib/util/timeseries/csv.co +++ /dev/null @@ -1,118 +0,0 @@ -_ = require 'kraken/util/underscore' -op = require 'kraken/util/op' - -TimeSeriesData = require 'kraken/util/timeseries/timeseries' - - -DASH_PATTERN = /-/g -BLANK_LINE_PATTERN = /^(\s*)$/ -COMMENT_PATTERN = /\s*(#|\/\/).*$/ - -class CSVData extends TimeSeriesData - DEFAULT_OPTIONS : - colSep : ',' - rowSep : '\n' - defaultType : 'float' - customBars : false - customSep : ';' - errorBars : false - fractions : false - fractionSep : '/' - skipBlankLines : true - blankLinePat : BLANK_LINE_PATTERN - removeCommentedText : true - commentPat : COMMENT_PATTERN - replaceMissing : false - replaceMissingValue : 0 - replaceNaN : false - replaceNaNValue : 0 - padRows : false - padRowsValue : 0 - - - (data, opts) -> - super ... - - - /* * * * CSV Parsing * * * */ - - parseNumber: (s) -> - parseFloat s - - parseHiLo: (s) -> - s.split @options.customBars .map @parseNumber, this - - parseFraction: (s) -> - s.split @options.fractionSep .map @parseNumber, this - - parseDate: (s) -> - new Date s.replace DASH_PATTERN, '/' - - - /** - * Parses and imports a CSV string. - * - * @private - * @returns {this} - */ - parse: (@rawData) -> - if typeof rawData is not 'string' - - o = @options - - lines = rawData.split o.rowSep - return [] unless lines.length - first = lines[0] - - # Use the default delimiter or fall back to a tab if that makes sense. - delim = o.colSep - if first.indexOf(delim) is -1 and first.indexOf('\t') >= 0 - delim = '\t' - - rows = @rows = [] - @columns = [] - - parser = @parseNumber - parser = @parseHiLo if o.customBars - parser = @parseFraction if o.fractions - - hasHeaders = @labels.length is not 0 - for line, i of lines - line .= replace o.commentPat, '' if o.removeCommentedText - continue if o.skipBlankLines and (line.length is 0 or o.blankLinePat.test line) - - cols = line.split delim - unless hasHeaders - hasHeaders = true - @labels = cols.map -> _.strip it - continue - - continue unless cols.length > 1 - date = @parseDate cols.shift() - fields = cols.map parser, this - if o.errorBars - fields = fields.reduce do - (acc, v) -> - last = acc[acc.length-1] - unless last and last.length < 2 - acc.push last = [] - last.push v - acc - [] - - fields.unshift date - rows.push fields - fields.forEach (v, idx) ~> - @columns.push [] unless @columns[idx] - @columns[idx].push v - - @untransformedRows = _.merge [], @rows - this - - - - - -module.exports = exports = CSVData - - diff --git a/lib/util/timeseries/index.co b/lib/util/timeseries/index.co deleted file mode 100644 index 6fa58c2..0000000 --- a/lib/util/timeseries/index.co +++ /dev/null @@ -1,2 +0,0 @@ -exports.TimeSeriesData = require 'kraken/timeseries/timeseries' -exports.CSVData = require 'kraken/timeseries/csv' diff --git a/lib/util/timeseries/timeseries.co b/lib/util/timeseries/timeseries.co deleted file mode 100644 index 216bc76..0000000 --- a/lib/util/timeseries/timeseries.co +++ /dev/null @@ -1,192 +0,0 @@ -_ = require 'kraken/util/underscore' -op = require 'kraken/util/op' - - - -/** - * @class Represents a collection of data columns aligned along a common timeline. - */ -class TimeSeriesData - DEFAULT_OPTIONS : {} - - options : {} - labels : [] - types : [] - - untransformedRows : null # row-oriented (untransformed) - rows : null # row-oriented - columns : null # column-oriented (includes date column) - dateColumn : null # only date column - dataColumns : null # column-oriented (excludes date column) - - - /** - * @constructor - */ - (data, opts) -> - unless typeof data is 'string' or _.isArray data - [opts, data] = [data, null] - @options = _.clone(@DEFAULT_OPTIONS) import (opts or {}) - - @transforms = [] - @labels = @options.labels or [] - @types = @options.types or [] - - @parse that if data or @options.data - @rebuildDerived() - - - /* * * * TimeSeriesData interface * * * */ - - - /** - * @returns {Array} List of rows, each of which includes all columns. - */ - getData: -> - @data - - /** - * @returns {Array} List of all columns (including date column). - */ - getColumns: -> - @columns - - /** - * @returns {Array} The date column. - */ - getDateColumn: -> - @dateColumn - - /** - * @returns {Array} List of all columns except the date column. - */ - getDataColumns: -> - @dataColumns - - /** - * @returns {Array} List of column labels. - */ - getLabels: -> - @labels - - - /* * * * Parsing * * * */ - - /** - * Subclass and override to perform preprocessing of the data. - * @private - */ - parse : (rawData) -> - this - - /** - * Rebuilds the row-oriented data matrix from the columns. - * @private - */ - rebuildData: -> - @rows = _.zip ...@columns - @rebuildDerived() - - /** - * Rebuilds the column-oriented data matrix from the columns. - * @private - */ - rebuildColumns: -> - @columns = _.zip ...@rows - @rebuildDerived() - - /** - * @private - */ - rebuildDerived: -> - while @transforms.length < @columns.length - @transforms.push [] - @dateColumn = @columns[0] - @dataColumns = @columns.slice(1) - this - - - - /* * * * Data Transformation * * * */ - - /** - * Applies the stack of transforms to the data. - * - * TODO: Apply transforms in @getData()? - * @private - * @returns {this} - */ - applyTransforms: -> - for fns, idx of @transforms - for fn of fns - @columns[idx] .= map fn, ctx - @rebuildData() - - /** - * Clears all transforms and restores the original data. - * @returns {this} - */ - clearTransforms: -> - @transforms = [] - @rows = _.merge [], @untransformedRows - @rebuildColumns() - - /** - * Add a data transform to the specified columns. The function is - * applied one-by-one (in column-major order), replacing the data - * with the mapped result. - * - * @param {Number|Array} indices List one or more column indices to map. Negative - * numbers are offset from the end of the columns list. - * @param {Function} fn Mapping function of the form: - * `(single_value, row_idx, column) -> new_value` - * @param {Object} [ctx=this] Execution context for the function. - * @returns {this} - */ - addTransform: (indices, fn, ctx=this) -> - num_cols = @columns.length - if typeof idx is 'function' - [ctx, fn, indices] = [fn, indices, null] - unless indices? - indices = _.range num_cols - unless _.isArray indices - indices = [indices] - for idx of indices - idx %= num_cols - idx += num_cols if idx < 0 - @transforms[idx].push fn - @applyTransforms() - - /** - * Add a data transform to all columns except the date column. The function - * is applied one-by-one (in column-major order), replacing the data - * with the mapped result. - * - * @param {Function} fn Mapping function of the form: - * `(single_value, row_idx, column) -> new_value` - * @param {Object} [ctx=this] Execution context for the function. - * @returns {this} - */ - addDataTransform: (fn, ctx=this) -> - @addTransform _.range(1, @columns.length), fn, ctx - - - - /* * * * Misc * * * */ - - /** - * @returns {Array} Deep copy of the data rows (including all columns). - */ - toJSON: -> - _.merge [], @getData() - - toString: -> - labels = @labels - .map -> "'#it'" - .join ', ' - "#{@..name or @..displayName}(#labels)" - - - -module.exports = exports = TimeSeriesData - -- 1.7.0.4