"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",
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 ...
_.pluck @subviews, 0 .forEach @removeSubview, this
this
+ bubbleEvent: (evt) ->
+ @invokeSubviews 'trigger', ...arguments
+ this
+
### Rendering Chain
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 : ->
className : 'data-ui'
template : require 'kraken/template/data'
- datasources : {}
+ data : {}
+ datasources : null
initialize: ->
@graph_id = @options.graph_id
BaseView::initialize ...
+ @datasources = @model.sources
@on 'ready', @onReady, this
@load()
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()
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()
*/
metrics : null
+ defaults : ->
+ palette : null
+ lines : []
+ metrics : []
+
constructor: function DataSet
BaseModel ...
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
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.
*/
onMetricChange: ->
- mx = @get 'metrics'
- cols = mx.columns.map (col) ->
- _.clone(col) import mx.defaults
- @metrics.reset cols
+ @metrics.reset @get 'metrics'
# }}}
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: ->
* @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: ->
timespan :
start : null
- stop : null
+ end : null
step : '1mo'
columns : []
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
*/
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
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
+
+
+
toTemplateLocals: ->
locals = @model.toJSON()
- locals import {
- @graph_id, @dataset, @datasources,
- source_summary : '<Select Source>'
- metric_summary : '<Select Metric>'
- timespan_summary : '<Select Timespan>'
- }
+ locals import {@graph_id, @dataset, @datasources}
+
+ ds = @model.source
+ hasSource = @model.get('source_id')? and ds
+ locals.source_summary = unless hasSource then '<Select Source>' else ds.get 'shortName'
+
+ hasMetric = hasSource and @model.get('source_col')?
+ locals.metric_summary = unless hasMetric then '<Select Metric>' 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 '<Select Timespan>' else "#{ts.start} — #{ts.end}"
+
+ locals
onHeaderClick: ->
@$el.toggleClass 'in'
toTemplateLocals: ->
locals = BaseView::toTemplateLocals ...
- locals import { @graph_id, @dataset, @datasources, }
+ locals import { @graph_id, @dataset, @datasources }
build: ->
BaseView::build ...
editMetric: (metric) ->
console.log "#this.editMetric!", metric
- @model = metric
+ @datasource_ui_view.model = @model = metric
@render()
@show()
this
} = require 'kraken/util'
{ BaseModel, BaseList,
} = require 'kraken/base'
+{ DataSource, DataSourceList,
+} = require 'kraken/dataset/datasource-model'
/**
*/
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
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
/**
*/
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'
# }}}
constructor: function MetricList then BaseList ...
initialize: -> BaseList::initialize ...
+ comparator: (metric) ->
+ metric.get('index') ? Infinity
+
# }}}
ChartOption, ChartOptionList, TagSet,
ChartOptionView, ChartOptionScaffold,
} = require 'kraken/chart'
+{ DataSource, DataSourceList,
+} = require 'kraken/dataset'
{ Graph, GraphList, GraphEditView,
} = require 'kraken/graph'
$ '#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()
.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}
+
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
+