--- /dev/null
+root = do -> this
+
+_ = require 'kraken/util/underscore'
+{ BaseView,
+} = require 'kraken/base'
+{ ChartType, DEBOUNCE_RENDER,
+} = require 'kraken/chart'
+{ Graph,
+} = require 'kraken/graph/graph-model'
+
+
+
+GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{
+ ctorName : 'GraphDisplayView'
+ tagName : 'section'
+ className : 'graph'
+ template : require 'kraken/template/graph-display'
+
+ # events :
+ # 'blur .value' : 'update'
+ # 'submit .value' : 'update'
+
+
+ initialize: ->
+ @model or= new Graph
+ BaseView::initialize ...
+
+
+ toString: -> "#{@ctorName}(#{@model})"
+# }}}
_ = require 'kraken/util/underscore'
{ BaseView,
} = require 'kraken/base'
-{ ChartOptionScaffold, ChartOption, ChartOptionList, DEBOUNCE_RENDER,
+{ ChartOptionScaffold, DEBOUNCE_RENDER,
} = require 'kraken/chart'
-{ VisModel,
-} = require 'kraken/vis/vis-model'
+{ Graph,
+} = require 'kraken/graph/graph-model'
/**
- * View for a graph visualization encapsulating the UI for:
+ * @class View for a graph visualization encapsulating the editing UI for:
* - Graph metadata, such as name, description, slug
+ * - Chart options, using ChartOptionScaffold
*/
-VisView = exports.VisView = BaseView.extend do # {{{
+GraphEditView = exports.GraphEditView = BaseView.extend do # {{{
FILTER_CHART_OPTIONS : <[
file labels visibility colors dateWindow ticker timingName xValueParser
axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter
__bind__ : <[
render renderAll resizeViewport
formatter axisFormatter
- onReady onModelChange onScaffoldChange onFirstClickRenderOptionsTab
+ onReady onSync
+ onModelChange onScaffoldChange onFirstClickRenderOptionsTab
]>
__debounce__: <[ render renderAll ]>
- ctorName : 'VisView'
+ ctorName : 'GraphEditView'
tagName : 'section'
- className : 'graph'
- template : require 'kraken/template/graph'
+ className : 'graph graph-edit'
+ template : require 'kraken/template/graph-edit'
events:
'click .redraw-button' : 'render'
'click .save-button' : 'save'
- # 'click .load-button' : 'load'
+ 'click .load-button' : 'load'
'keypress form.details input[type="text"]' : 'onKeypress'
'keypress form.options .value' : 'onKeypress'
'submit form.details' : 'onDetailsSubmit'
initialize : (o={}) ->
- @model or= new VisModel
+ @model or= new Graph
BaseView::initialize ...
# console.log "#this.initialize!"
# Note: can't debounce the method itself, as the debounce wrapper returns undefined
$ root .on 'resize', _.debounce(@resizeViewport, DEBOUNCE_RENDER)
- @id = _.domize 'graph', (@model.get('slug', @model.id or @model.cid))
+ @id = _.domize 'graph', (@model.get('slug') or @model.id or @model.cid)
- @model.on 'destroy', @remove, this
- @model.on 'change', @render, this
- @model.on 'change:dataset', @onModelChange
- @model.on 'change:options', @onModelChange
+ @model
+ .on 'ready', @onReady
+ .on 'sync', @onSync
+ .on 'destroy', @remove, this
+ .on 'change', @render, this
+ .on 'change:dataset', @onModelChange
+ .on 'change:options', @onModelChange
+ .on 'error', ~>
+ console.error "#this.error!", arguments
+ # TODO: UI alert
# Rerender the options boxes once the tab is visible
@$el.on 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab
@scaffold = new ChartOptionScaffold
@$el.find '.graph-options-pane' .append @scaffold.el
@scaffold.collection.reset that if o.graph_spec
-
@scaffold.on 'change', @onScaffoldChange
- options = @model.getOptions()
- @chartOptions options, {+silent}
-
+ @chartOptions @model.getOptions(), {+silent}
@resizeViewport()
- _.delay @onReady, DEBOUNCE_RENDER
+ # _.delay @onReady, DEBOUNCE_RENDER
- change: ->
- @model.change()
- @scaffold.invoke 'change'
- this
+
+
+ load: ->
+ console.log "#this.load!"
+ @model.fetch()
save: ->
console.log "#this.save!"
- $.ajax do
- url : '/graph/save'
- type : 'POST'
- data : @toJSON()
- success : (response) ->
- console.log 'saved!'
- error : (err) ->
- console.error "error!", arguments
+ id = @model.id or @model.get('slug')
+ @model.save {id}, {+wait}
+ change: ->
+ @model.change()
+ @scaffold.invoke 'change'
+ this
+
chartOptions: (values, opts) ->
# Handle @chartOptions(k, v, opts)
if arguments.length > 1 and typeof values is 'string'
delete options[k]
options
+
+ toTemplateLocals: ->
+ attrs = _.clone @model.attributes
+ delete attrs.options
+ { $, _, op, @model, view:this } import attrs
+
+
/**
* Resizes chart according to the model's width and height.
* @return { width, height }
size
+ # Repopulate UI from Model
+ renderDetails: ->
+ form = @$el.find 'form.details'
+ for k, v in @model.attributes
+ continue if k is 'options'
+ txt = @model.serialize v
+
+ el = form.find "input[name=#k]"
+ if el.attr('type') is 'checkbox'
+ el.attr 'checked', if v then 'checked' else ''
+ else
+ el.val txt
+
+ form.find "textarea[name=#k]" .text txt
+ this
+
render: ->
return this unless @ready
+ @renderDetails()
dataset = @model.get 'dataset'
size = @resizeViewport()
# @chart.resize size
@updateURL()
+ @trigger 'render', this
this
renderAll: ->
### Event Handlers {{{
onReady: ->
- # console.log "(#this via VisView).ready!"
+ console.log "(#this via GraphEditView).ready!"
@ready = @scaffold.ready = true
+ @onSync()
+
+ onSync: ->
+ return unless @ready
+ console.info "#this.sync() --> success!"
+ # TODO: UI alert
# @change()
- @model.change()
+ # @model.change()
+ @chartOptions @model.getOptions(), {+silent}
@renderAll()
onModelChange: ->
changes = @model.changedAttributes()
- # console.log 'VisModel.changed( options ) ->', JSON.stringify changes
+ options = @model.getOptions()
+ # console.log "Graph.changed( options ) ->\n\tchanges: #{JSON.stringify changes}\n\toptions: #{JSON.stringify options}" #"\n\t^opts: #{JSON.stringify _.intersection _.keys(changes), _.keys(options)}"
@chart.updateOptions file:that if changes?.dataset
- @chartOptions that, {+silent} if changes?.options
+ @chartOptions options, {+silent} if changes?.options
onScaffoldChange: (scaffold, value, key, field) ->
current = @model.getOption(key)
* Represents a Graph, including its charting options, dataset, annotations, and all
* other settings for both its content and presentation.
*/
-VisModel = exports.VisModel = BaseModel.extend do # {{{
- ctorName : 'VisModel'
+Graph = exports.Graph = BaseModel.extend do # {{{
+ ctorName : 'Graph'
IGNORE_OPTIONS : <[ width height timingName ]>
urlRoot : '/graph'
- idAttribute : 'slug'
ready : false
/**
/**
* List of graph parents.
- * @type VisList
+ * @type GraphList
*/
parents : null
- constructor : (attributes={}, options) ->
- # @on 'ready', ~> console.log "(#this via VisModel).ready!"
+ constructor : (attributes={}, opts) ->
+ @on 'ready', ~> console.log "(#this via Graph).ready!"
attributes.options or= {}
@optionCascade = new Cascade attributes.options
- BaseModel.call this, attributes, options
+ BaseModel.call this, attributes, opts
- initialize : ->
+ initialize : (attributes, opts) ->
@__super__.initialize ...
+ opts = {+autoLoad} import (opts or {})
- @parents = new VisList
+ @constructor.register this
+ @parents = new GraphList
# TODO: Load on-demand
@chartType = ChartType.lookup @get('chartType')
# unless @id or @get('id') or @get('slug')
# @set 'slug', "unsaved_graph_#{@cid}"
- @constructor.register this
@trigger 'init', this
- @load()
+ @load() if opts.autoLoad
load: (opts={}) ->
return this if @ready and not opts.force
+ self = this
@trigger 'load', this
- Seq @get('parents')
- .seqMap -> VisModel.lookup it, this
+ Seq()
+ .seq_ (next) ~>
+ if @isNew()
+ next.ok()
+ else
+ console.log "#{this}.fetch()..."
+ @fetch do
+ error : (err) ~>
+ console.error "#{this}.fetch() --> error! #arguments"
+ next.ok()
+ success : (model, res) ~>
+ console.log "#{this}.fetch() --> success!", res
+ next.ok res
+ .seq_ (next) ~>
+ next.ok @get('parents')
+ .flatten()
+ .seqMap -> Graph.lookup it, this
.seqEach_ (next, parent) ~>
@parents.add parent
@optionCascade.addLookup parent.get('options')
.seq ~>
@ready = true
@trigger 'ready', this
+
+ this
### Accessors
values = { "#key": value }
values = @parse values
- if @ready and values.options
+ setter = (@__super__ or BaseModel::).set
+
+ # Merge options in, firing granulated change events
+ if values.options
+ # Remove from values to prevent the super call to `set()` from
+ # replacing the object wholesale.
options = delete values.options
+
+ # ...Unless we don't have one yet.
+ if not @attributes.options
+ setter.call this, {options}, {+silent}
+
+ # Now delegate `setOption()` to do the nested merging.
@setOption options, opts
- (@__super__ or BaseModel::).set.call this, values, opts
+ # Deal with everything else
+ setter.call this, values, opts
@optionCascade.isChangedValue k
and not @isDefaultOption k
+
toJSON: (opts={}) ->
- opts = {+keepDefaults} import opts
-
- # use jQuery's deep-copy implementation
- json = $.extend true, {}, @attributes
- # json = _.clone(@attributes) import { options:_.clone(@attributes.options) }
- return json if opts.keepDefaults
-
- for k, v in json.options
- delete json.options[k] if v is void or @isDefaultOption k
- json
+ opts = {+keepDefaults, +keepUnchanged} import opts
+ # use jQuery's deep-copy implementation -- XXX: Deep-copy no longer necessary thanks to @getOptions()
+ # json = $.extend true, {}, @attributes
+ json = _.clone(@attributes) import { options:@getOptions(opts) }
toKVPairs: (opts={}) ->
opts = {-keepSlug, -keepDefaults, -keepUnchanged} import opts
# use jQuery's deep-copy implementation
- kvo = $.extend true, {}, @attributes
+ kvo = @toJSON opts
kvo.parents = JSON.stringify kvo.parents
delete kvo.slug unless opts.keepSlug
# console.group 'toKVPairs'
# console.log '[IN]', JSON.stringify kvo
- kvo.options = @getOptions opts
for k, v in kvo.options
kvo.options[k] = @serialize v
# console.log '[OUT]', JSON.stringify kvo
# console.groupEnd()
+
_.collapseObject kvo
toKV: (opts) ->
# }}}
-VisList = exports.VisList = BaseList.extend do # {{{
- ctorName : 'VisList'
+GraphList = exports.GraphList = BaseList.extend do # {{{
+ ctorName : 'GraphList'
urlRoot : '/graph'
- model : VisModel
+ model : Graph
initialize : ->
BaseList::initialize ...
/* * * * Visualization Cache for parent-lookup * * * {{{ */
-VIS_CACHE = exports.VIS_CACHE = new VisList
+GRAPH_CACHE = exports.GRAPH_CACHE = new GraphList
-VisModel import do
- CACHE : VIS_CACHE
+Graph import do
+ CACHE : GRAPH_CACHE
register: (model) ->
- # console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model
- unless @CACHE.contains model
- @CACHE.add model
+ console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model
+ if @CACHE.contains model
+ @CACHE.remove model, {+silent}
+ @CACHE.add model
model
get: (id) ->
cb null, that
else
Cls = this
- new Cls { id, slug:id } .fetch do
- success : -> cb null, it
- error : cb
+ @register new Cls { id, slug:id }
+ .on 'ready', -> cb null, it
-# models = require 'kraken/graph/graph-model'
-# views = require 'kraken/graph/graph-view'
-# exports import models import views
+models = require 'kraken/graph/graph-model'
+display_views = require 'kraken/graph/graph-display-view'
+edit_views = require 'kraken/graph/graph-edit-view'
+exports import models import display_views import edit_views
ChartOption, ChartOptionList, TagSet,
ChartOptionView, ChartOptionScaffold,
} = require 'kraken/chart'
-{ VisView, VisModel, VisList,
-} = require 'kraken/vis'
+{ Graph, GraphList, GraphEditView,
+} = require 'kraken/graph'
root = this
CHART_OPTIONS_SPEC = []
CHART_DEFAULT_OPTIONS = {}
-ROOT_VIS_DATA = {}
-ROOT_VIS_OPTIONS = {}
# Create the Graph Scaffold
main = ->
- # opts = root.CHART_DEFAULT_OPTIONS = {}
- # for opt of root.CHART_OPTIONS_SPEC
- # opts[opt.name] = opt.default
-
+ # Set up Dygraph chart type spec
+ # TODO: load this on-demand
dyglib = new DygraphsChartType CHART_OPTIONS_SPEC
- # 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!\n\n', String(root.location), '\n\n'
- data = {}
# Process URL
loc = String root.location
+ data = {}
# If we got querystring args, apply them to the graph
if loc.split '?' .1
data.options or {}
(v, k) -> [ k, dyglib.parseOption(k,v) ]
- # Extract slug from URL
+ # Extract id from URL
if match = /\/graph\/(?!view)([^\/?]+)/i.exec loc
- data.slug = match[1]
+ data.id = data.slug = match[1]
# _.dump _.clone(data.options), 'data.options'
- vis = root.vis = new VisModel data, {+parse}
- graph = root.graph = new VisView do
- graph_spec : root.CHART_OPTIONS_SPEC
- model : vis
- $ '#content .inner' .append graph.el
+ # Instantiate model & view
+ graph = root.graph = new Graph data, {+parse}
+ view = root.view = new GraphEditView do
+ graph_spec : root.CHART_OPTIONS_SPEC # FIXME: necessary?
+ model : graph
+
+ $ '#content .inner' .append view.el
# Load data files
-Seq([ <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>,
- <[ ROOT_VIS_DATA /presets/root.json ]>
+Seq([ <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>
])
.parEach_ (next, [key, url]) ->
jQuery.ajax do
- var id = model.id || model.cid
- var graph_id = view.id
-section.graph(id=graph_id)
+section.graph.graph-edit(id=graph_id)
form.details.form-horizontal
.name-row.row-fluid.control-group
.row-fluid
.half.control-group
.control-group
- label.slug.control-label(for='slug') Slug
+ label.slug.control-label(for="#{id}_slug") Slug
.controls
- input.span3.slug(type='text', id='slug', name='slug', placeholder='graph_slug', value=slug)
+ input.span3.slug(type='text', id="#{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.
.control-group
- label.width.control-label(for='width') Size
+ label.width.control-label(for="#{id}_width") Size
.controls
- input.span1.width(type='text', id='width', name='width', value=width)
+ input.span1.width(type='text', id="#{id}_width", name='width', value=width)
| ×
- input.span1.height(type='text', id='height', name='height', value=height)
+ input.span1.height(type='text', id="#{id}_height", name='height', value=height)
p.help-block Choosing 'auto' will size the graph to the viewport bounds.
.half.control-group
- label.desc.control-label(for='desc') Description
+ label.desc.control-label(for="#{id}_desc") Description
.controls
//- textarea.span3.desc(id='desc', name='desc', placeholder='Graph description.') #{desc}
- <textarea class="span3 desc" id="desc" name="desc" placeholder="Graph description.">#{desc}</textarea>
+ <textarea class="span3 desc" id="#{id}_desc" name="desc" placeholder="Graph description.">#{desc}</textarea>
p.help-block A description of the graph.
.graph-data-pane.tab-pane(id="graph-#{graph_id}-data")
.row-fluid
- label.dataset.control-label(for='dataset') Data Set
+ label.dataset.control-label(for="#{id}_dataset") Data Set
.controls
- input.span3.dataset(type='text', id='dataset', name='dataset', placeholder='URL to dataset file', value=dataset)
+ input.span3.dataset(type='text', id="#{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-#{graph_id}-options")
for o of arguments then @set o
this
- toObject: ->
+ toJSON: ->
_.extend {}, ...@_lookups.slice().reverse()
# XXX: should unique? but then won't map 1:1 to @values()...
"#{Cls.displayName or Cls.name}()"
+ALIASES =
+ toJSON : 'toObject'
+ each : 'forEach'
+
+for src, dest in ALIASES
+ Cascade::[dest] = Cascade::[src]
module.exports = exports = Cascade
+++ /dev/null
-models = require 'kraken/vis/vis-model'
-views = require 'kraken/vis/vis-view'
-exports import models import views
ChartOption, ChartOptionList, TagSet,
ChartOptionView, ChartOptionScaffold,
} = require 'kraken/chart'
-{ VisView, VisModel, VisList,
-} = require 'kraken/vis'
+{ GraphEditView, Graph, GraphList,
+} = require 'kraken/graph'
root = this
CHART_OPTIONS_SPEC = []
CHART_DEFAULT_OPTIONS = {}
-ROOT_VIS_DATA = {}
-ROOT_VIS_OPTIONS = {}
# Create the Graph Scaffold
main = ->
- # opts = root.CHART_DEFAULT_OPTIONS = {}
- # for opt of root.CHART_OPTIONS_SPEC
- # opts[opt.name] = opt.default
-
dyglib = new DygraphsChartType CHART_OPTIONS_SPEC
- # 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!\n\n', String(root.location), '\n\n'
if match = /\/graph\/(?!view)([^\/?]+)/i.exec loc
data.slug = match[1]
- vis = root.vis = new VisModel data, {+parse}
- # graph = root.graph = new VisView do
+ vis = root.vis = new Graph data, {+parse}
+ # graph = root.graph = new GraphEditView do
# graph_spec : root.CHART_OPTIONS_SPEC
# model : vis
# $ '#content .inner' .append graph.el
# Load data files
Seq([ <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>,
- <[ ROOT_VIS_DATA /presets/root.json ]>
])
.parEach_ (next, [key, url]) ->
jQuery.ajax do
- template:
- chart-option.jade
- chart-scaffold.jade
- - graph.jade
- # - graph:
- # - graph-model
- # - graph-view
- # - index
- - vis:
- - vis-model
- - vis-view
+ - graph-edit.jade
+ - graph-display.jade
+ - graph:
+ - graph-model
+ - graph-edit-view
+ - graph-display-view
- index
# - suffix: .js