From: dsc Date: Mon, 16 Apr 2012 20:08:18 +0000 (-0700) Subject: Checkpoint on Data UI. X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=d71ed4f9818653176ff4e00dfc3ae254f519709a;p=kraken-ui.git Checkpoint on Data UI. --- diff --git a/data/graphs/ohai.json b/data/graphs/ohai.json index 1be77f0..6dbc35f 100644 --- a/data/graphs/ohai.json +++ b/data/graphs/ohai.json @@ -7,22 +7,19 @@ "dataset": "/data/datasources/rc/rc_page_requests.csv", "data" : { - "metrics" : { - "defaults" : { - "source_id" : "rc_page_requests" - }, - "columns" : [ - { - "source_col" : 1, - "label" : "All Wikipedias (+Mobile)", - "color" : "#E62F74" - }, { - "source_col" : 2, - "label" : "English", - "color" : "#244792" - } - ] - } + "metrics" : [ + { + "source_id" : "rc_page_requests", + "source_col" : 1, + "label" : "All Wikipedias (+Mobile)", + "color" : "#E62F74" + }, { + "source_id" : "rc_page_requests", + "source_col" : 2, + "label" : "English", + "color" : "#244792" + } + ] }, "width": "auto", diff --git a/lib/base/base-view.co b/lib/base/base-view.co index 8172727..7d22832 100644 --- a/lib/base/base-view.co +++ b/lib/base/base-view.co @@ -15,16 +15,32 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ tagName : 'section' /** + * 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" + * @type String + */ + __view_type_id__: null + + /** * Array of [view, selector]-pairs. * @type Array<[BaseView, String]> */ subviews : [] + /** + * Whether this view has been added to the DOM. + * @type Boolean + */ + _parented: false + constructor : function BaseView @__class__ = @constructor @__superclass__ = @..__super__.constructor + @__view_type_id__ or= _.str.underscored @getClassName() .replace /_view$/, '' @waitingOn = 0 @subviews = [] Backbone.View ... @@ -90,6 +106,10 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ _.pluck @subviews, 0 .forEach @removeSubview, this this + bubbleEvent: (evt) -> + @invokeSubviews 'trigger', ...arguments + this + ### Rendering Chain @@ -123,14 +143,32 @@ BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ 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 + this + + remove : -> + @undelegateEvents() + @$el.remove() + return this unless @_parented + @_parented = false + @trigger 'unparent', this + this ### UI Utilities - hide : -> @$el.hide(); this - show : -> @$el.show(); this - remove : -> @$el.remove(); this - clear : -> @model.destroy(); @remove() + hide : -> @$el.hide(); @trigger('hide', this); this + show : -> @$el.show(); @trigger('show', this); this + clear : -> @model.destroy(); @trigger('clear', this); @remove() # remove : -> diff --git a/lib/dataset/data-view.co b/lib/dataset/data-view.co index 298b6d2..d031f97 100644 --- a/lib/dataset/data-view.co +++ b/lib/dataset/data-view.co @@ -18,7 +18,8 @@ DataView = exports.DataView = BaseView.extend do # {{{ className : 'data-ui' template : require 'kraken/template/data' - datasources : {} + data : {} + datasources : null @@ -28,6 +29,7 @@ DataView = exports.DataView = BaseView.extend do # {{{ initialize: -> @graph_id = @options.graph_id BaseView::initialize ... + @datasources = @model.sources @on 'ready', @onReady, this @load() @@ -50,8 +52,9 @@ DataView = exports.DataView = BaseView.extend do # {{{ load: -> @wait() - $.getJSON '/datasources/all', (@datasources) ~> - _.each @datasources, @canonicalizeDataSource, this + $.getJSON '/datasources/all', (@data) ~> + _.each @data, @canonicalizeDataSource, this + @model.sources.reset _.map @data, -> it @ready = true @unwait() @render() @@ -81,7 +84,7 @@ DataView = exports.DataView = BaseView.extend do # {{{ toTemplateLocals: -> attrs = _.clone @model.attributes - { $, _, op, @model, view:this, @graph_id, @datasources, } import attrs + { $, _, op, @model, view:this, @graph_id, @datasources } import attrs # attachSubviews: -> # @$el.empty() diff --git a/lib/dataset/dataset-model.co b/lib/dataset/dataset-model.co index fc8983c..0cef0ad 100644 --- a/lib/dataset/dataset-model.co +++ b/lib/dataset/dataset-model.co @@ -29,6 +29,11 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ */ metrics : null + defaults : -> + palette : null + lines : [] + metrics : [] + constructor: function DataSet BaseModel ... @@ -36,24 +41,21 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ initialize : -> BaseModel::initialize ... @sources = new DataSourceList - mx = @attributes.metrics or= {} - @metrics = new MetricList mx.columns + @metrics = new MetricList @attributes.metrics @on 'change:metrics', @onMetricChange, this - defaults : -> - palette : null - lines : [] - metrics : - defaults : {} - columns : [] - load: (opts={}) -> return this if @ready and not opts.force @wait() @trigger 'load', this - Seq() + Seq @metrics.pluck 'source_id' + .parMap_ (next, source_id) -> + DataSource.lookup source_id, next + .seqEach_ (next, source) ~> + @sources.add source + next.ok source .seq ~> @ready = true @trigger 'ready', this @@ -61,6 +63,10 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ this + # TODO: toJSON() must ensure columns in MetricList are ordered by index + # ...in theory, MetricList.comparator now does this + + /** * @returns {Array} The reified dataset, materialized to an array of data-series arrays. */ @@ -74,10 +80,7 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ onMetricChange: -> - mx = @get 'metrics' - cols = mx.columns.map (col) -> - _.clone(col) import mx.defaults - @metrics.reset cols + @metrics.reset @get 'metrics' # }}} diff --git a/lib/dataset/dataset-view.co b/lib/dataset/dataset-view.co index aeb6910..efb7d0d 100644 --- a/lib/dataset/dataset-view.co +++ b/lib/dataset/dataset-view.co @@ -28,9 +28,10 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{ BaseView::initialize ... @views_by_cid = {} @addAllMetrics() - @model.metrics.on 'add', @addMetric, this - @model.metrics.on 'remove', @removeMetric, this - @model.metrics.on 'reset', @addAllMetrics, this + @model.metrics + .on 'add', @addMetric, this + .on 'remove', @removeMetric, this + .on 'reset', @addAllMetrics, this newMetric: -> diff --git a/lib/dataset/datasource-model.co b/lib/dataset/datasource-model.co index 61a9660..7d3ef82 100644 --- a/lib/dataset/datasource-model.co +++ b/lib/dataset/datasource-model.co @@ -8,14 +8,28 @@ * @class */ DataSource = exports.DataSource = BaseModel.extend do # {{{ + __bind__ : <[ onLoadSuccess onLoadError ]> urlRoot : '/datasources' + ready : false + + /** + * Parsed data for this datasource. + * @type Array + */ + data : null + + + constructor: function DataSource BaseModel ... initialize: -> + @attributes = @canonicalize @attributes BaseModel::initialize ... + @constructor.register this + @on 'load-success', @onLoadSuccess, this defaults: -> @@ -32,7 +46,7 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{ timespan : start : null - stop : null + end : null step : '1mo' columns : [] @@ -44,10 +58,83 @@ DataSource = exports.DataSource = BaseModel.extend do # {{{ url: -> "/datasources/#{@id}.json" + 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 + + load: -> + @trigger 'load', this + url = @get 'url' + switch @get 'format' + case 'json' + @loadJSON url + case 'csv' + @loadCSV url + default + console.error "#this.load() Unknown Data Format!" + @trigger 'load-error', this, 'Unknown Data Format!' + this + + onLoadSuccess: -> + return if @ready + @ready = true + @trigger 'ready', this + + onLoadError: (jqXHR, txtStatus, err) -> + @_loadError = true + console.error "#this Error loading data! -- #msg: #{err or ''}" + + + loadJSON: (url) -> + $.ajax do + url : url + dataType : 'json' + success : (@data) -> + @trigger 'load-success', this + error : (jqXHR, txtStatus, err) -> + @trigger 'load-error', this, txtStatus, err + this + + loadCSV: (url) -> + $.ajax do + url : url + dataType : 'text' + success : (@data) -> + @trigger 'load-success', this + error : (jqXHR, txtStatus, err) -> + @trigger 'load-error', this, txtStatus, err + this + + parseCSV: (data) -> + ... + + + getColumnName: (idx) -> + @get('metrics')?[idx]?.label + + getColumnIndex: (name) -> + return that.idx if _.find @get('metrics'), -> it.label is name + -1 + + # }}} - /** * @class */ @@ -57,15 +144,16 @@ DataSourceList = exports.DataSourceList = BaseList.extend do # {{{ constructor: function DataSourceList then BaseList ... initialize : -> BaseList::initialize ... - - # }}} + + /* * * * DataSource Cache * * * */ DataSource import do CACHE : new DataSourceList + ready : false register: (model) -> # console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model @@ -77,16 +165,30 @@ DataSource import do get: (id) -> @CACHE.get id - lookup: (id, cb) -> + lookup: (id, cb, cxt=this) -> # console.log "#{@CACHE}.lookup(#id, #{typeof cb})" + unless @ready + @on 'cache-ready', ~> + @off 'cache-ready', arguments.callee + @lookup id, cb, cxt + return + if @CACHE.get id - cb null, that + cb.call cxt, null, that else Cls = this @register new Cls {id} - .on 'ready', -> cb null, it + .on 'ready', -> cb.call cxt, null, it _.bindAll DataSource, 'register', 'get', 'lookup' +# Fetch all DataSources +$.getJSON '/datasources/all', (data) -> + DataSource.CACHE.reset _.map data, -> it + DataSource.ready = true + DataSource.trigger 'cache-ready', DataSource + + + diff --git a/lib/dataset/datasource-ui-view.co b/lib/dataset/datasource-ui-view.co index 43b92b9..affc7d5 100644 --- a/lib/dataset/datasource-ui-view.co +++ b/lib/dataset/datasource-ui-view.co @@ -31,12 +31,21 @@ DataSourceUIView = exports.DataSourceUIView = BaseView.extend do # {{{ toTemplateLocals: -> locals = @model.toJSON() - locals import { - @graph_id, @dataset, @datasources, - source_summary : '' - timespan_summary : '' 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' diff --git a/lib/dataset/metric-edit-view.co b/lib/dataset/metric-edit-view.co index 45ca185..b8f743b 100644 --- a/lib/dataset/metric-edit-view.co +++ b/lib/dataset/metric-edit-view.co @@ -36,7 +36,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ toTemplateLocals: -> locals = BaseView::toTemplateLocals ... - locals import { @graph_id, @dataset, @datasources, } + locals import { @graph_id, @dataset, @datasources } build: -> BaseView::build ... @@ -46,7 +46,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ editMetric: (metric) -> console.log "#this.editMetric!", metric - @model = metric + @datasource_ui_view.model = @model = metric @render() @show() this diff --git a/lib/dataset/metric-model.co b/lib/dataset/metric-model.co index b3630bd..8d22de8 100644 --- a/lib/dataset/metric-model.co +++ b/lib/dataset/metric-model.co @@ -2,6 +2,8 @@ } = require 'kraken/util' { BaseModel, BaseList, } = require 'kraken/base' +{ DataSource, DataSourceList, +} = require 'kraken/dataset/datasource-model' /** @@ -16,25 +18,16 @@ Metric = exports.Metric = BaseModel.extend do # {{{ */ source : null - - constructor: function Metric - BaseModel ... - - initialize : -> - BaseModel::initialize ... - - defaults : -> index : 0 label : 'New Metric' type : 'int' - timespan : { start:null, stop:null, step:null } + timespan : { start:null, end:null, step:null } disabled : false # DataSource - source_id : null - source_col_idx : -1 - source_col_name : '' + source_id : null + source_col : -1 # Chart Options color : null @@ -42,8 +35,37 @@ Metric = exports.Metric = BaseModel.extend do # {{{ format_value : null format_axis : null - scale : 1.0 transforms : [] + scale : 1.0 + + + + constructor: function Metric + BaseModel ... + + initialize : -> + BaseModel::initialize ... + @on 'change:source_id', @onUpdateSource, this + @onUpdateSource() + + + onUpdateSource: -> + if source_id = @get 'source_id' + @wait() + DataSource.lookup source_id, @onSourceReady, this + this + + onSourceReady: (err, source) -> + console.log "#this.onSourceReady", arguments + @unwait() + if err + console.error "#this Error loading DataSource! #err" + else + @source = source + unless @ready + @ready = true + @trigger 'ready', this + this /** @@ -52,10 +74,11 @@ Metric = exports.Metric = BaseModel.extend do # {{{ */ isOk: -> (label = @get('label')) and label is not 'New Metric' - and @get('source_id') and @get('source_col_idx') >= 0 + and @get('source_id') and @get('source_col') >= 0 and _.every @get('timespan'), op.ok - + getSourceColumnName: -> + @source?.getColumnName @get 'source_col' # }}} @@ -70,4 +93,7 @@ MetricList = exports.MetricList = BaseList.extend do # {{{ constructor: function MetricList then BaseList ... initialize: -> BaseList::initialize ... + comparator: (metric) -> + metric.get('index') ? Infinity + # }}} diff --git a/lib/main-edit.co b/lib/main-edit.co index e9e431d..2bd6679 100644 --- a/lib/main-edit.co +++ b/lib/main-edit.co @@ -11,6 +11,8 @@ Backbone = require 'backbone' ChartOption, ChartOptionList, TagSet, ChartOptionView, ChartOptionScaffold, } = require 'kraken/chart' +{ DataSource, DataSourceList, +} = require 'kraken/dataset' { Graph, GraphList, GraphEditView, } = require 'kraken/graph' @@ -60,14 +62,12 @@ main = -> $ '#content .inner' .append view.el - # Load data files Seq([ <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]> ]) .parEach_ (next, [key, url]) -> jQuery.ajax do url : url, - dataType : 'json' success : (res) -> root[key] = res next.ok() diff --git a/lib/template/datasource-ui.jade b/lib/template/datasource-ui.jade index 2f53006..83be803 100644 --- a/lib/template/datasource-ui.jade +++ b/lib/template/datasource-ui.jade @@ -16,28 +16,41 @@ section.datasource-ui .tabbable.tabs-left ul.datasource-sources-list.nav.nav-tabs li: h6 Data Sources - for ds, k in datasources + for source, k in datasources.models + - var ds = source.attributes + - var activeClass = (source_id === ds.id ? 'active' : '') - var ds_target = "#"+graph_id+" .datasource-ui .datasource-selector .datasource-source.datasource-source-"+ds.id - li: a(href="#datasource-selector_datasource-source-#{ds.id}", data-toggle="tab", data-target=ds_target) #{ds.shortName} + li(class=activeClass): a(href="#datasource-selector_datasource-source-#{ds.id}", data-toggle="tab", data-target=ds_target) #{ds.shortName} .datasource-sources-details.tab-content - for ds, k in datasources - .datasource-source.tab-pane(class="datasource-source-#{ds.id}") - .datasource-source-details - .source-name #{ds.name} - .source-id #{ds.id} - .source-url #{ds.url} - .source-format #{ds.format} - .source-charttype #{ds.chart.chartType} - .datasource-source-time - .source-time-start #{ds.timespan.start} - .source-time-end #{ds.timespan.end} - .source-time-step #{ds.timespan.step} - ol.datasource-source-metrics - for m, idx in ds.metrics.slice(1) - li.datasource-source-metric - span.source-metric-idx #{m.idx} - | - span.source-metric-label #{m.label} - | - span.source-metric-type #{m.type} + for source, k in datasources.models + - var ds = source.attributes + - var activeClass = (source_id === ds.id ? 'active' : '') + .datasource-source.tab-pane(class="datasource-source-#{ds.id} #{activeClass}") + .well + .datasource-source-details + .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 + h6 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/www/css/data.styl b/www/css/data.styl index a3c4212..7a83db6 100644 --- a/www/css/data.styl +++ b/www/css/data.styl @@ -186,15 +186,39 @@ section.graph section.data-ui height 100% display table-cell float none - // width 200px - // min-width 200px li a min-width 150px + .well + padding 0.75em + .datasource-sources-details display table-cell padding 1em - // width auto + .source-id, .source-format, .source-charttype, + thead, .source-metric-idx, .source-metric-type + display none + .source-name + font-size 15px + margin-bottom 0.75em + .source-url + display block + width 95% + font-family menlo, monospace + .datasource-source-time + margin 0.5em + & > * + display inline-block + margin-right 1em + .datasource-source-metric + &, &:hover + cursor pointer + &.active td + color #3a87ad + background-color #d9edf7 + border 1px solid #3a87ad + border-width 1px 0 +