ctorName : 'BaseModel'
initialize: ->
- _.bindAll this, ...@__bind__ if @__bind__.length
@__super__ = @constructor.__super__
+ _.bindAll this, ...@__bind__ if @__bind__.length
+
+ serialize: (v) ->
+ if v!?
+ v = ''
+ else if _.isBoolean v
+ v = Number v
+ else if _.isObject v
+ v = JSON.stringify v
+ String v
/**
* Like `.toJSON()` in that it should return a plain object with no functions,
* @returns {Object}
*/
toKVObject: ->
- _.collapseObject @toJSON()
+ kvo = _.collapseObject @toJSON()
+ for k, v in kvo
+ kvo[k] = @serialize v
+ kvo
/**
* Serialize the model into a `www-form-encoded` string suitable for use as
_.bindAll this, ...@__bind__ if @__bind__.length
@__super__ = @constructor.__super__
+ @build()
@model.view = this
@$el.data { @model, view:this }
@model.on 'change', @render, this
{ Field, FieldList, FieldView, Scaffold,
} = require 'kraken/scaffold'
+IGNORED_TAGS = exports.IGNORED_TAGS = <[ callback deprecated debugging ]>
+
class exports.TagSet extends Array
# Notify Tag indexer of category when created, to ensure all category-tags
# get indices with colors :P
KNOWN_TAGS.update @getCategory()
+
+ # Ignore functions/callbacks and, ahem, hidden tags.
+ type = @get 'type', '' .toLowerCase()
+ tags = @get 'tags', []
+ if _.str.include(type, 'function') or _.intersection tags, IGNORED_TAGS .length
+ @set 'ignore', true
# Wrapper to ensure @set('tags') is called, as tags.push()
ctorName : 'GraphOptionsScaffold'
tagName : 'form'
className : 'options scaffold'
+ template : require 'kraken/template/graph-scaffold'
collectionType : GraphOptionList
subviewType : GraphOptionView
+ $fields : '.fields'
# GraphView will set this
ready : false
# console.log "#this.render() -> .isotope()"
@__super__.render ...
return this unless @ready
- @$el.isotope do
+ @$el.find '.options.control-group' .isotope do
itemSelector : '.field.option'
layoutMode : 'masonry'
masonry : columnWidth : 10
+Seq = require 'seq'
{ _, op,
} = require 'kraken/util'
{ BaseView, BaseModel, BaseList,
{ VisView, VisModel,
} = require 'kraken/vis'
-root = do -> this
+root = this
+CHART_OPTIONS_SPEC = []
+ROOT_VIS_DATA = {}
+ROOT_VIS_OPTIONS = {}
+
# Create the Graph Scaffold
main = ->
+ # TODO: create a preset manager
+ # Remove chart options from data so we don't have to deepcopy
+ ROOT_VIS_OPTIONS := delete root.ROOT_VIS_DATA.options
+
+ # Bind to URL changes
History.Adapter.bind window, 'statechange', ->
- console.log 'StateChange!', String root.location
+ console.log 'StateChange!', String(root.location)
- graph = root.graph = new VisView do
- graph_spec : CHART_OPTIONS_SPEC
# If we got querystring args, apply them to the graph
- if root.location?.search.slice 1
- g = _.uncollapseObject _.fromKVPairs that
- # yarr, have to do options separately or everything goes to shit
- options = delete g.options
- graph.model.set g, {+silent}
- graph.chartOptions options, {+silent}
+ data = {}
+ if String(root.location).split '?' .1
+ data = _.uncollapseObject _.fromKVPairs that.replace('#', '%23')
- # Flush all changes once the DOM settles
- _.defer do
- -> graph.scaffold.invoke 'change'
- 50
+ # # yarr, have to do options separately or everything goes to shit
+ # options = delete data.options
+ # graph.model.set data, {+silent}
+ # graph.chartOptions options, {+silent}
+ # # Flush all changes once the DOM settles
+ # _.delay do
+ # ->
+ # graph.change()
+ # # graph.scaffold.invoke 'change'
+ # 50
+
+
+ vis = root.vis = new VisModel data
+ graph = root.graph = new VisView do
+ graph_spec : root.CHART_OPTIONS_SPEC
+ model : vis
$ '#content .inner' .append graph.el
-jQuery.ajax do
- url : '/graph/dygraph-options.json',
- dataType : 'json'
- success : (data) ->
- root.CHART_OPTIONS_SPEC = data
- jQuery main
- error : (err) -> console.error err
+
+# Load data files
+Seq([ <[ CHART_OPTIONS_SPEC /graph/dygraph-options.json ]>,
+ <[ ROOT_VIS_DATA /graph/root ]>
+])
+.parEach_ (next, [key, url]) ->
+ jQuery.ajax do
+ url : url,
+ dataType : 'json'
+ success : (data) ->
+ root[key] = data
+ next.ok()
+ error : (err) -> console.error err
+.seq ->
+ console.log 'All data loaded!'
+ jQuery main
initialize: ->
+ _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse'))
@set 'value', @get('default'), {+silent} if not @has 'value'
# console.log "#this.initialize!"
# @on 'all', (evt) ~> console.log "#this.trigger(#evt)"
type = _ (type or @get 'type').toLowerCase()
for t of <[ Integer Float Boolean Object Array Function ]>
if type.startsWith t.toLowerCase()
- # console.log "parse#t ->", @["parse#t"] unless @["parse#t"]
return @["parse#t"]
@parseString
parseFunction: (fn) ->
if fn and _.startswith String(fn), 'function'
- try eval "(#fn)" catch err
+ try eval "(#fn)" catch err then null
else
null
/* * * Serializers * * */
serializeValue: ->
- v = @getValue()
- if v!?
- v = ''
- else if _.isBoolean v
- v = Number v
- else if _.isArray(v) or _.isObject(v)
- v = JSON.stringify v
- String v
+ @serialize @getValue()
toJSON: ->
{id:@id} import do
_.clone(@attributes) import { value:@getValue(), def:@get('default') }
toKVObject: ->
- { "#{@id}": @serializeValue() }
+ { "#{@id}":@serializeValue() }
toString: -> "(#{@id}: #{@serializeValue()})"
# }}}
* Collects a map of fields to their values, excluding those set to `null` or their default.
* @returns {Object}
*/
- values: (keepDefaults=false) ->
+ values: (opts={}) ->
+ opts = {-keepDefaults, -serialize} import opts
_.synthesize do
- if keepDefaults then @models else @models.filter -> not it.isDefault()
- -> [ it.get('name'), it.getValue() ]
+ 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()
toKVObject: ->
- _.collapseObject @toJSON()
+ _.collapseObject @values false, true
toKVPairs: (item_delim='&', kv_delim='=') ->
_.toKVPairs @toKVObject(), item_delim, kv_delim
@trigger 'update', this
render: ->
- return @remove() if @model.get 'hidden', false
+ return @remove() if @model.get 'ignore', false
return BaseView::render ... if @template
name = @model.get 'name'
addOne: (field) ->
# console.log "[S] #this.addOne!", @__super__
+ fields = if @$fields then @$el.find that else @$el
_.remove @subviews, field.view if field.view
# avoid duplicating event propagation
SubviewType = @subviewType
view = new SubviewType model:field
@subviews.push view
- @$el.append view.render().el unless field.get 'hidden'
+ fields.append view.render().el unless field.get 'ignore'
view.on 'update', @change.bind(this, field)
@render()
fs = require 'fs'
path = require 'path'
+{existsSync:exists} = path
{exec, spawn} = require 'child_process'
_ = require 'underscore'
# Allow "spoofing" HTTP methods that IE doesn't support
app.use express.methodOverride()
+ # Route to the web services
+ app.use app.router
+
# Transparently recompile modules that have changed
app.use compiler do
enabled : <[ coco jade-browser stylus yaml ]>
app.use express.static VAR
app.use express.static STATIC
- # Route to the web services
- app.use app.router
-
# Serve directory listings
app.use express.directory WWW
app.use express.directory VAR
app.get '/', (req, res) ->
res.render 'dashboard'
+app.get '/graph/:id', (req, res, next) ->
+ {id} = req.params
+ console.log req.url
+ if exists("#WWW/graph/#id.yaml") or exists("#WWW/graph/#id.json")
+ req.url += '.json'
+ next()
+
YAML_EXT_PAT = /\.ya?ml$/i
console.error that if err.stack
res.send { error:String(err), partial_data:data }
+
app.get '/:type/:action', (req, res, next) ->
{type, action} = req.params
if path.existsSync "#WWW/#type/#action.jade"
else
next()
+
+
+
+
/**
* Handle webhook notification to pull from origin.
*/
- var id = model.id || model.cid, graph_id = _.domize('graph', id)
section.graph(id=graph_id)
- .graph-label
- .viewport
-
form.details.form-horizontal
.name-row.row-fluid.control-group
//- label.name.control-label(for="#{id}_name"): h3 Graph Name
- .controls: input.span6.name(type='text', id="#{id}_name", placeholder='Graph Name', value=name)
+ input.span6.name(type='text', id="#{id}_name", name="name", placeholder='Graph Name', value=name)
+
+ .viewport
+ .graph-label
.row-fluid
.half.control-group
label.slug.control-label(for='slug') Slug
.controls
- input.span3.slug(type='text', id='slug', placeholder='graph_slug', value=slug)
+ input.span3.slug(type='text', id='slug', name='slug', placeholder='graph_slug', value=slug)
p.help-block The slug uniquely identifies this graph and will be displayed in the URL.
.half.control-group
label.dataset.control-label(for='dataset') Data Set
.controls
- input.span3.dataset(type='text', id='dataset', placeholder='URL to dataset file', value=dataset)
+ input.span3.dataset(type='text', 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.
.row-fluid
.half.control-group
label.width.control-label(for='width') Width
.controls
- input.span1.width(type='text', id='width', value=width)
+ input.span1.width(type='text', id='width', name='width', value=width)
p.help-block Choosing 'auto' will size the graph to the viewport bounds.
.half.control-group
label.height.control-label(for='height') Height
- .controls: input.span1.height(type='text', id='height', value=height)
+ .controls: input.span1.height(type='text', id='height', name='height', value=height)
- fieldset.options
- legend Graph Options
-
+
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' then JSON.parse(v) else v
# comparison
cmp : (x,y) -> if x < y then -1 else (if x > y then 1 else 0)
{ BaseModel, BaseView,
} = require 'kraken/base'
+root = do -> this
/**
*/
VisModel = exports.VisModel = BaseModel.extend do # {{{
ctorName : 'VisModel'
- urlRoot : '/graphs'
+ urlRoot : '/graph'
idAttribute : 'slug'
initialize : ->
+ BaseModel::initialize ...
name = @get 'name'
- if name and not (@id or @get('slug'))
+ if name and not @get 'slug', @id
@set 'slug', _.underscored name
+
defaults: ->
{
slug : ''
name : ''
- dataset : '/data/pageviews_by.timestamp.language.csv'
desc : ''
+ dataset : '/data/pageviews_by.timestamp.language.csv'
+ # presets : []
width : 'auto'
height : 320
- options : {}
- }
+ options : {} import root.ROOT_VIS_OPTIONS
+ } import root.ROOT_VIS_DATA
+
+ parse: (data) ->
+ data = JSON.parse data if typeof data is 'string'
+ for k, v in data
+ data[k] = Number v if _.contains(<[ width height ]>, k) and v is not 'auto'
+ data
+
+ set: (values, opts) ->
+ if arguments.length > 1 and typeof values is 'string'
+ [k, v, opts] = arguments
+ values = { "#k": v }
+ BaseModel::set.call this, @parse(values), opts
+
+
+ ### Chart Option Accessors ###
hasOption: (key) ->
options = @get 'options', {}
@trigger "change:options:#key", this, value, key, opts unless opts.silent
+
toString: -> "#{@ctorName}(id=#{@id}, name=#{@get 'name'}, dataset=#{@get 'dataset'})"
# }}}
template : require 'kraken/template/graph'
events:
- 'keypress form.options .value' : 'onKeypress'
- 'submit form.options' : 'onSubmit'
+ 'keypress form.details input[type="text"]' : 'onKeypress'
+ 'keypress form.options .value' : 'onKeypress'
+ 'submit form.details' : 'onDetailsSubmit'
+ 'submit form.options' : 'onOptionsSubmit'
ready: false
# console.log 'Model.changed(options) ->', changes
@chartOptions changes, {+silent}
- @build()
@viewport = @$el.find '.viewport'
@scaffold = new GraphOptionsScaffold
- @$el.find 'fieldset' .append @scaffold.el
+ @$el.append @scaffold.el
@scaffold.collection.reset that if o.graph_spec
@scaffold.on 'change', (scaffold, value, key, field) ~>
options = @model.get 'options', {}
@chartOptions options, {+silent}
+ @resizeViewport()
_.delay @onReady, DEBOUNCE_RENDER
onReady: ->
+ console.log 'VisView.ready!'
@ready = @scaffold.ready = true
+ @change()
@renderAll()
+ change: ->
+ @model.change()
+ @scaffold.invoke 'change'
+ this
+
chartOptions: (values, opts) ->
# Handle @chartOptions(k, v, opts)
options.get(k)?.setValue v, opts
this
else
- options.values()
+ options.values() # TODO: pull this from model (must clone), sort out events
/**
* @return { width, height }
*/
resizeViewport: ->
- return this unless @ready and @chart
+ return this unless @ready
# Remove old style, as it confuses dygraph after options update
@viewport.attr 'style', ''
height = @viewport.height()
size = { width, height }
@viewport.css size
- console.log 'resizeViewport!', JSON.stringify(size), @viewport
+ # console.log 'resizeViewport!', JSON.stringify(size), @viewport
# @chart.resize size if forceRedraw
size
+
render: ->
return this unless @ready
-
- # console.log "#this"
- # console.log do
- # " .viewport.{ width=%s, height=%s, style=%s }"
- # @viewport.css('width')
- # @viewport.css('height')
- # @viewport.attr 'style'
- # console.log ' .options:', JSON.stringify options
+ console.log 'VisView.render!'
size = @resizeViewport()
- options = @chartOptions()
+ options = @chartOptions() import size
+ options.labelsDiv = @$el.find '.graph-label' .0
+
# @chart?.destroy()
unless @chart
@chart = new Dygraph do
@chart.updateOptions options
@chart.resize size
- path = root.location?.path or '/'
- url = "#path?#{@toKVPairs()}"
- # console.log 'History.pushState', url
- History.pushState url, @model.get('name', root.document?.title or ''), url
+ # path = String(root.location?.path or '/')
+ data = @toJSON()
+ title = @model.get('name', root.document?.title or '')
+ url = "?"+@toKVPairs()
+ # console.log 'History.pushState', JSON.stringify(data), title, url
+ History.pushState data, title, url
this
renderAll: ->
return this unless @ready
+ console.log 'VisView.renderAll!'
_.invoke @scaffold.subviews, 'render'
@scaffold.render()
@render()
onKeypress: (evt) ->
$(evt.target).submit() if evt.keyCode is 13
- onSubmit: ->
- # console.log "#this.onSubmit!"
+ onDetailsSubmit: ->
+ console.log "#this.onDetailsSubmit!"
+ data = _.synthesize do
+ @$el.find('form.details').serializeArray()
+ -> [it.name, it.value]
+ console.log @$el, JSON.stringify data
+ @model.set data
+ false
+
+ onOptionsSubmit: ->
+ console.log "#this.onOptionsSubmit!"
@render()
false
+ toJSON: ->
+ @model.toJSON()
+
toKVPairs: ->
@model.toKVPairs.apply @model, arguments
/* background colors */
-$main_bgcolor = white
-$page_bgcolor = $light
+$main_bgcolor = $light
+$page_bgcolor = white
$foot_bgcolor = #3b3b3b
/* text & link colors */
position relative
.graph-label
- position relative
+ position absolute
+ z-index 100
+ top 1em
+ right 1em
+ max-width 300px
+
+ padding 1em
+ border-radius 5px
+ background-color rgba(255,255,255, 0.75)
+ font 12px/1.3 "helvetica neue", helvetica, arial, sans-serif
+
+ .viewport:hover + .graph-label
+ border 1px solid $light
.viewport
position relative
margin-bottom 1.5em
overflow hidden
+
form.details
position relative
.name-row
+ font-size 120%
line-height 2em
- // input.name
- // width 50%
+ input.name
+ font-size 120%
+ line-height 1.2
+ height 1.2em
+ border-color $light
+ // width 50%
.row-fluid
.half.control-group
width 50%
font-size 11px
line-height 1.3
- fieldset.options
- border-radius 5px
- legend
- width auto
- display inline-block
+ .options fieldset
+ border 0px
.field.option
float left
mixin css('graph.css')
mixin css('isotope.css')
-
body
block body
- header.navbar.navbar-fixed-top: .navbar-inner: .container
- block header
- h1: a(href="/") Kraken
- block nav
- nav#menu: ul
- block menu-items
- li.home Home
+ //-
+ header
+ block header
+ h1: a(href="/") Kraken
+ block nav
+ nav#menu: ul
+ block menu-items
+ li.home Home
section#content
- .spacer
+ //- .spacer
.inner
block content
- template:
- graph.jade
- graph-option.jade
+ - graph-scaffold.jade
- scaffold:
- scaffold-model
- scaffold-view