From: dsc Date: Thu, 29 Mar 2012 08:38:57 +0000 (-0700) Subject: Graphs now cascade settings, to enable 'Fork this Graph', as well as inheritence... X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=bd12ae5bf16ff3c2b48f28df20c3be0175f93cf2;p=limn-bak.git Graphs now cascade settings, to enable 'Fork this Graph', as well as inheritence from root graph settings. --- diff --git a/lib/chart/chart-library.co b/lib/chart/chart-library.co index edba6e3..dee4ece 100644 --- a/lib/chart/chart-library.co +++ b/lib/chart/chart-library.co @@ -4,8 +4,6 @@ op = require 'kraken/util/op' {Parsers, ParserMixin} = require 'kraken/util/parser' -KNOWN_LIBRARIES = exports.KNOWN_LIBRARIES = {} - /** * @class Specification for an option. */ @@ -32,19 +30,24 @@ class exports.ChartOption parse : Parsers.parseString isDefault: (v) -> - @default is v + # @default is v + _.isEqual @default, v toString: -> "(#{@name}: #{@type})" + +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_LIBRARIES = exports.KNOWN_LIBRARIES = {} + + /** * @class Abstraction of a charting library, encapsulating its logic and options. */ class exports.ChartLibrary extends EventEmitter - - @lookupLibrary = (name) -> - KNOWN_LIBRARIES[name] - /** * Ordered ChartOption objects. * @type ChartOption[] @@ -68,9 +71,7 @@ class exports.ChartLibrary extends EventEmitter (@name, options) -> @options_ordered = _.map options, (opt) ~> new ChartOption this, opt @options = _.synthesize @options_ordered, -> [it.name, it] - - # register library - KNOWN_LIBRARIES[@name] = this + ChartLibrary.register this /** @@ -80,7 +81,7 @@ class exports.ChartLibrary extends EventEmitter @options[name] or def /** - * @returns {Array} List of values found at the given attr on each + * @returns {Array} List of values found at the given attr on each * option spec object. */ pluck: (attr) -> @@ -112,6 +113,8 @@ class exports.ChartLibrary extends EventEmitter String v + ### Parsers + /** * When implementing a ChartLibrary, you can add or override parsers * merely by subclassing. @@ -124,5 +127,29 @@ class exports.ChartLibrary extends EventEmitter parseOption: (name, value) -> @getParserFor(name)(value) + parseOptions: (options) -> + out = {} + for k, v in options + out[k] = @parseOption k, v + out + + + + ### Class Methods + + /** + * Register a new library. + */ + @register = (library) -> + KNOWN_LIBRARIES[library.name] = library + + /** + * Look up a library by name. + */ + @lookupLibrary = (name) -> + KNOWN_LIBRARIES[name] + + + diff --git a/lib/main.co b/lib/main.co index e621416..fe468c1 100644 --- a/lib/main.co +++ b/lib/main.co @@ -12,7 +12,7 @@ Backbone = require 'backbone' { GraphOption, GraphOptionList, GraphOptionView, GraphOptionsScaffold, TagSet, } = require 'kraken/graph' -{ VisView, VisModel, +{ VisView, VisModel, VisList, } = require 'kraken/vis' @@ -48,6 +48,7 @@ main = -> # If we got querystring args, apply them to the graph if loc.split '?' .1 data = _.uncollapseObject _.fromKV that.replace('#', '%23') + data.parents = JSON.parse that if data.parents data.options = _.synthesize do data.options or {} (v, k) -> [ k, dyglib.parseOption(k,v) ] @@ -56,6 +57,7 @@ main = -> if match = /\/graph\/(?!view)([^\/?]+)/i.exec loc 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 @@ -73,8 +75,8 @@ Seq([ <[ CHART_OPTIONS_SPEC /schema/dygraph.json ]>, jQuery.ajax do url : url, dataType : 'json' - success : (data) -> - root[key] = data + success : (res) -> + root[key] = res next.ok() error : (err) -> console.error err .seq -> diff --git a/lib/vis/vis-model.co b/lib/vis/vis-model.co index 59365e8..20929f1 100644 --- a/lib/vis/vis-model.co +++ b/lib/vis/vis-model.co @@ -1,113 +1,237 @@ +Seq = require 'seq' + _ = require 'kraken/underscore' -{ BaseModel, BaseView, +Cascade = require 'kraken/util/cascade' +{ ChartLibrary, +} = require 'kraken/chart' +{ BaseModel, BaseView, BaseList, } = require 'kraken/base' root = do -> this + /** * 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' IGNORE_OPTIONS : <[ width height timingName ]> - ctorName : 'VisModel' - urlRoot : '/graph' - idAttribute : 'slug' + urlRoot : '/graph' + idAttribute : 'slug' + ready : false + /** + * The chart type backing this graph. + * @type ChartLibrary + */ + library : null - initialize : -> - BaseModel::initialize ... - name = @get 'name' - if name and not @get 'slug', @id - @set 'slug', _.underscored name + /** + * List of graph parents. + * @type VisList + */ + parents : null + /** + * Cascade of objects for options lookup (includes own options). + * @type Cascade + * @private + */ + optionCascade : null + + + /** + * Attribute defaults. + */ defaults: -> - ({ + { slug : '' name : '' desc : '' dataset : '/data/non_mobile_pageviews_by.timestamp.language.csv' - # presets : [] width : 'auto' height : 320 - } import root.ROOT_VIS_DATA) import { options:_.clone root.ROOT_VIS_OPTIONS } + library : 'dygraphs' + parents : <[ root ]> + options : {} + } + url: -> + "#{@urlRoot}/#{@get('slug')}.json" - 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 + + + constructor : (attributes={}, options) -> + # @on 'ready', ~> console.log "(#this via VisModel).ready!" + attributes.options or= {} + @optionCascade = new Cascade attributes.options + BaseModel.call this, attributes, options + + + initialize : -> + @__super__.initialize ... + + @parents = new VisList + # TODO: Load on-demand + @library = ChartLibrary.lookupLibrary @get('library') + + # unless @id or @get('id') or @get('slug') + # @set 'slug', "unsaved_graph_#{@cid}" + + @constructor.register this + @trigger 'init', this + @load() + + + load: (opts={}) -> + return this if @ready and not opts.force + + @trigger 'load', this + Seq @get('parents') + .seqMap -> VisModel.lookup it, this + .seqEach_ (next, parent) ~> + @parents.add parent + @optionCascade.addLookup parent.get('options') + next.ok() + .seq ~> + @ready = true + @trigger 'ready', this + + + ### Accessors + + get: (key) -> + if _.startsWith key, 'options.' + @getOption key.slice(8) + else + (@__super__ or BaseModel::).get.call this, key + + + set: (key, value, opts) -> + # Handle @set(values, opts) + if _.isObject(key) and key? + [values, opts] = [key, value] + else + values = { "#key": value } + values = @parse values + + if @ready and values.options + options = delete values.options + @setOption options, opts + + (@__super__ or BaseModel::).set.call this, values, opts + ### Chart Option Accessors ### hasOption: (key) -> - options = @get 'options', {} - options[key]? + @getOption(key) is void getOption: (key, def) -> - @get('options', {})[key] ? def + @optionCascade.get key, def setOption: (key, value, opts={}) -> - options = @get 'options', {} - unless _.contains @IGNORE_OPTIONS, key - options[key] = value - @set 'options', options, opts - @trigger "change:options:#key", this, value, key, opts unless opts.silent + if _.isObject(key) and key? + [values, opts] = [key, value or {}] + else + values = { "#key": value } + + _.dump values, "#this.setOption" + options = @get('options') + changed = false + for key, value in values + continue if _.contains @IGNORE_OPTIONS, key + changed = true + _.setNested options, key, value, {+ensure} + @trigger "change:options.#key", this, value, key, opts unless opts.silent + + if changed and not opts.silent + @trigger "change:options", this, options, 'options', opts this unsetOption: (key, opts={}) -> - options = @get 'options', {} - delete options[key] - @set 'options', options, opts - @trigger "change:options:#key", this, value, key, opts unless opts.silent + unless @optionCascade.unset(key) is void or opts.silent + @trigger "change:options.#key", this, void, key, opts + @trigger "change:options", this, @get('options'), 'options', opts this + getOptions: (opts={}) -> + opts = {+keepDefaults, +keepUnchanged} import opts + options = @optionCascade.toObject() + for k, v in options + delete options[k] if v is void or + (not opts.keepDefaults and @isDefaultOption k) or + (not opts.keepUnchanged and not @isChangedOption k) + options + + + ### Serialization + + parse: (data) -> + data = JSON.parse data if typeof data is 'string' + for k, v in data + data[k] = Number v if v is not 'auto' and _.contains <[ width height ]>, k + # data[k] = JSON.stringify v if k is 'parents' + data + + /** + * @returns {Boolean} Whether the value for option `k` is inherited or not. + */ + isOwnOption: (k) -> + @optionCascade.isOwnValue k + + /** + * @returns {Boolean} Whether the value for option `k` is the graph default or not. + */ + isDefaultOption: (k) -> + @library.isDefault k, @getOption k - ### URL Serialization + /** + * Whether the value for option `k` differs from that of its parent graphs. + * @returns {Boolean} + */ + isChangedOption: (k) -> + @optionCascade.isChangedValue k + and not @isDefaultOption k - toJSON: (options={}) -> - options = {+keepDefaults} import options + toJSON: (opts={}) -> + opts = {+keepDefaults} import opts - json = _.clone(@attributes) import { options:_.clone(@attributes.options) } - return json if options.keepDefaults + # use jQuery's deep-copy implementation + json = $.extend true, {}, @attributes + # json = _.clone(@attributes) import { options:_.clone(@attributes.options) } + return json if opts.keepDefaults - dyglib = ChartLibrary.lookupLibrary 'dygraphs' - opts = json.options - for k, v in opts - delete opts[k] if dyglib.isDefault k, v + for k, v in json.options + delete json.options[k] if v is void or @isDefaultOption k json - toKVPairs: (keepSlug=false) -> - kvo = @toJSON() - delete kvo.slug unless keepSlug + toKVPairs: (opts={}) -> + opts = {-keepSlug, -keepDefaults, -keepUnchanged} import opts + + # use jQuery's deep-copy implementation + kvo = $.extend true, {}, @attributes + kvo.parents = JSON.stringify kvo.parents + delete kvo.slug unless opts.keepSlug + # console.group 'toKVPairs' # console.log '[IN]', JSON.stringify kvo - opts = kvo.options = _.clone kvo.options - for k, rootVal in root.ROOT_VIS_OPTIONS - v = opts[k] - # console.log " [#k] rootVal:", rootVal, "===", v, "?", _.isEqual(rootVal, v) unless _.isEqual(rootVal, v) - if v is void or _.isEqual rootVal, v - delete opts[k] - else - opts[k] = @serialize v + 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: (keepSlug=false) -> - _.toKV @toKVPairs keepSlug + toKV: (opts) -> + _.toKV @toKVPairs opts /** @@ -116,10 +240,54 @@ VisModel = exports.VisModel = BaseModel.extend do # {{{ toURL: -> slug = @get 'slug', '' slug = "/#slug" if slug - "/graph#slug?#{@toKV(false)}" + "/graph#slug?#{@toKV { keepSlug: !!slug }}" - toString: -> "#{@ctorName}(id=#{@id})" + toString: -> "#{@ctorName}(id=#{@id}, cid=#{@cid})" +# }}} + +VisList = exports.VisList = BaseList.extend do # {{{ + ctorName : 'VisList' + urlRoot : '/graph' + model : VisModel + + initialize : -> + BaseList::initialize ... + + toString: -> + modelIds = _.pluck @models, 'id' + .map -> "\"#it\"" + .join ', ' + "#{@ctorName}(#modelIds)" # }}} +/* * * * Visualization Cache for parent-lookup * * * {{{ */ + +VIS_CACHE = exports.VIS_CACHE = new VisList + +VisModel import do + CACHE : VIS_CACHE + + register: (model) -> + # console.log "#{@CACHE}.register(#{model.id or model.get('id')})", model + unless @CACHE.contains model + @CACHE.add model + model + + get: (id) -> + @CACHE.get id + + lookup: (id, cb) -> + # console.log "#{@CACHE}.lookup(#id, #{typeof cb})" + if @CACHE.get id + cb null, that + else + Cls = this + new Cls { id, slug:id } .fetch do + success : -> cb null, it + error : cb + + + +/* }}} */ diff --git a/lib/vis/vis-view.co b/lib/vis/vis-view.co index 1927620..7ca9cec 100644 --- a/lib/vis/vis-view.co +++ b/lib/vis/vis-view.co @@ -20,8 +20,8 @@ _ = require 'kraken/underscore' */ VisView = exports.VisView = BaseView.extend do # {{{ FILTER_CHART_OPTIONS : <[ - file labels visibility colors dateWindow ticker timingName xValueParser - axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter + file labels visibility colors dateWindow ticker timingName xValueParser + axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter valueFormatter xValueFormatter yValueFormatter ]> __bind__ : <[ @@ -79,7 +79,7 @@ VisView = exports.VisView = BaseView.extend do # {{{ @scaffold.on 'change', @onScaffoldChange - options = @model.get 'options', {} + options = @model.getOptions() @chartOptions options, {+silent} @resizeViewport() @@ -115,7 +115,7 @@ VisView = exports.VisView = BaseView.extend do # {{{ fields.get(k)?.setValue v, opts this else - options = @model.toJSON({ -keepDefaults })?.options or {} + options = @model.getOptions {-keepDefaults, +keepUnchanged} for k of @FILTER_CHART_OPTIONS # console.log "filter #k?", not options[k] if k in options and not options[k] @@ -166,7 +166,7 @@ VisView = exports.VisView = BaseView.extend do # {{{ # valueFormatter : @formatter # console.log "#this.render!", dataset - # _.dump options, 'options' + _.dump options, 'options' # Always rerender the chart to sidestep the case where we need to push defaults into # dygraphs to reset the current option state. @@ -189,7 +189,7 @@ VisView = exports.VisView = BaseView.extend do # {{{ renderAll: -> return this unless @ready - console.log "#this.renderAll!" + # console.log "#this.renderAll!" _.invoke @scaffold.subviews, 'render' @scaffold.render() @render() @@ -227,9 +227,10 @@ VisView = exports.VisView = BaseView.extend do # {{{ ### Event Handlers {{{ onReady: -> - console.log "#this.ready!" + # console.log "(#this via VisView).ready!" @ready = @scaffold.ready = true - @change() + # @change() + @model.change() @renderAll() onModelChange: -> @@ -239,8 +240,21 @@ VisView = exports.VisView = BaseView.extend do # {{{ @chartOptions that, {+silent} if changes?.options onScaffoldChange: (scaffold, value, key, field) -> - # console.log "scaffold.change!", key, value - @model.setOption key, value, {+silent} #unless field.isDefault() + current = @model.getOption(key) + # console.log do + # "scaffold.change! #key:" + # current + # '-->' + # value + # " ( isDefault?" + # (current is void and field.isDefault()) + # "isEqual?" + # _.isEqual(value, current) + # ") --> " + # unless _.isEqual(value, current) or (current is void and field.isDefault()) then 'CHANGE' else 'SQUELCH' + + unless _.isEqual(value, current) or (current is void and field.isDefault()) + @model.setOption(key, value, {+silent}) onFirstClickRenderOptionsTab: -> @$el.off 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab