From 94af8eceef1b66acc01f5ed39f9e63d7381452a2 Mon Sep 17 00:00:00 2001 From: David Schoonover Date: Sun, 3 Jun 2012 14:53:20 +0200 Subject: [PATCH] Adds AppView to start cleaning up -main.co files; Begins refactor of chart-type code; fixes new, broken changes from browserify. --- docs/framework.md | 2 +- docs/internals/d3-mvc.md | 29 ++++ docs/internals/dataset-spec.yaml | 5 +- lib/app.co | 52 ++++++ lib/base/asset-manager.co | 43 +++++ lib/base/base-model.co | 28 +++- lib/chart/chart-option-model.co | 218 ------------------------- lib/chart/chart-option-view.co | 270 ------------------------------- lib/chart/chart-type.co | 28 +++- lib/chart/dygraphs.co | 147 ----------------- lib/chart/index.co | 10 +- lib/chart/option/chart-option-model.co | 218 +++++++++++++++++++++++++ lib/chart/option/chart-option-view.co | 270 +++++++++++++++++++++++++++++++ lib/chart/option/index.co | 4 + lib/chart/type/d3/d3-geo-chart-type.co | 182 +++++++++++++++++++++ lib/chart/type/d3/index.co | 5 + lib/chart/type/dygraphs.co | 142 ++++++++++++++++ lib/graph/graph-model.co | 24 ++-- lib/main-edit.co | 14 +- lib/server/server.co | 5 +- msc/list-option-tags.py | 32 ++++ package.co | 2 +- www/css/geo-display.styl | 2 +- www/modules.yaml | 11 +- www/schema/d3/d3-geo-world.yaml | 99 +++++++++++ 25 files changed, 1165 insertions(+), 677 deletions(-) create mode 100644 docs/internals/d3-mvc.md create mode 100644 lib/app.co create mode 100644 lib/base/asset-manager.co delete mode 100644 lib/chart/chart-option-model.co delete mode 100644 lib/chart/chart-option-view.co delete mode 100644 lib/chart/dygraphs.co create mode 100644 lib/chart/option/chart-option-model.co create mode 100644 lib/chart/option/chart-option-view.co create mode 100644 lib/chart/option/index.co delete mode 100644 lib/chart/type/d3-bar.co delete mode 100644 lib/chart/type/d3-geo.co delete mode 100644 lib/chart/type/d3-line.co create mode 100644 lib/chart/type/d3/d3-bar-chart-type.co create mode 100644 lib/chart/type/d3/d3-geo-chart-type.co create mode 100644 lib/chart/type/d3/d3-line-chart-type.co create mode 100644 lib/chart/type/d3/index.co create mode 100644 lib/chart/type/dygraphs.co create mode 100755 msc/list-option-tags.py create mode 100644 www/schema/d3/d3-geo-world.yaml diff --git a/docs/framework.md b/docs/framework.md index 93cc3bb..88ba7d5 100644 --- a/docs/framework.md +++ b/docs/framework.md @@ -49,7 +49,7 @@ Notes from working with Backbone, Bootstrap, and other friends. - `TableView` (and all manner of stupid subclasses) - `Form{List,Dict}View` - `TabPane{List,Dict}View` -- All Views should have a `StateMachine` (also supporting the EventEmitter API) to ease coordinating event flows +- All Views should have a `StateMachine` (also supporting the `EventEmitter` API) to ease coordinating event flows - Significantly simplifies "loading" and "waiting" states - Trivial to "disable" elements - Easy modal UIs diff --git a/docs/internals/d3-mvc.md b/docs/internals/d3-mvc.md new file mode 100644 index 0000000..f83f243 --- /dev/null +++ b/docs/internals/d3-mvc.md @@ -0,0 +1,29 @@ +# d3.mvc + +An MVC framework built on top of `d3`. + + +## d3.model + +d3.model(attrs) -> d3.Model + +### Attribute Accessors + +- attr() -> Map +- attr(k) -> * +- attr(k, v) -> this +- has(k) -> Boolean +- get(k, [default]) -> * +- set(k, v) -> this +- del(k) -> this + + + + + +## d3.view + + + + + diff --git a/docs/internals/dataset-spec.yaml b/docs/internals/dataset-spec.yaml index b113386..7679ca0 100644 --- a/docs/internals/dataset-spec.yaml +++ b/docs/internals/dataset-spec.yaml @@ -31,8 +31,11 @@ timespan : step : Amount of time between elapsed between each row, measured in seconds, eg: 86400 ### Metadata about the Datatypes -# Note: 'columns' can also be supplied as a list of { label, type } objects instead of two lists. (TODO) +# Note: 'columns' can also be supplied as a list of { label, type, id } objects instead of two lists. (TODO) columns : + ids : List of the column-ids in order. This is like column-label, except it must be unique + for the dataset. This allows charts to specify their metrics by (dataset, col_id) + rather than column-index, making the chart independent of column-order. labels : List of the column-names in order. (Future optimization: the date column could be omitted if the data is sorted and contains no gaps -- each datapoint is exactly timespan.step apart, and none are missing.) diff --git a/lib/app.co b/lib/app.co new file mode 100644 index 0000000..58e16ad --- /dev/null +++ b/lib/app.co @@ -0,0 +1,52 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' + + +/** + * @class Application view, automatically attaching to an existing element + * found at `appSelector`. + * @extends Backbone.View + */ +AppView = exports.AppView = Backbone.View.extend do # {{{ + appSelector : '#content .inner' + + + /** + * @constructor + */ + constructor: function AppView (options={}) + if typeof options is 'function' + @initialize = options + options = {} + else + @initialize = that if options.initialize + + @appSelector = that if options.appSelector + options.el or= jQuery @appSelector .0 + console.log "new #this", options + Backbone.View.call this, options + + jQuery ~> @render() + this + + /** + * Override to set up your app. This method may be passed + * as an option to the constructor. + */ + initialize: -> # stub + + /** + * Append subviews. + */ + render : -> + @$el.append @view.el if @view and not @view.$el.parent()?.length + + getClassName: -> + "#{@..name or @..displayName}" + + toString: -> + "#{@getClassName()}()" +# }}} + diff --git a/lib/base/asset-manager.co b/lib/base/asset-manager.co new file mode 100644 index 0000000..fa91012 --- /dev/null +++ b/lib/base/asset-manager.co @@ -0,0 +1,43 @@ +{ _, op, +} = require 'kraken/util' +{ ReadyEmitter, +} = require 'kraken/util/event' + + + + +class AssetManager extends ReadyEmitter + # Map from key/url to data. + assets : null + + + /** + * @constructor + */ + -> + super ... + @assets = {} + + + + + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + load: -> + return this if @ready + proto = @constructor:: + jQuery.ajax do + url : @SPEC_URL + success : (spec) ~> + proto.spec = spec + proto.options_ordered = spec + proto.options = _.synthesize spec, -> [it.name, it] + proto.ready = true + @emit 'ready', this + error: ~> console.error "Error loading #{@typeName} spec! #it" + this + + + diff --git a/lib/base/base-model.co b/lib/base/base-model.co index 5048a1d..260c398 100644 --- a/lib/base/base-model.co +++ b/lib/base/base-model.co @@ -87,14 +87,24 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ * @param {Object} [opts={}] Options: * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context. * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load. - * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed. - * @param {Boolean} [opts.force=false] If true, move forward with the load even if we're ready. - * @returns {this} + * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed successfully. + * @param {String} [opts.errorEvent='load-error'] Event which signals loading has completed but failed. + * @param {Boolean} [opts.force=false] If true, reset ready state if we're ready before proceeding. + * @param {Boolean} [opts.readyIfError=false] If true, move fire the ready event when loading completes, even if it failed. + * @returns {this} */ loader: (opts={}) -> - opts = { -force, startEvent:'load', completeEvent:'load-success', ...opts } + opts = { + -force + -readyIfError + startEvent : 'load' + completeEvent : 'load-success' + errorEvent : 'load-error' + ...opts + } @resetReady() if opts.force - return this if not opts.start or @loading or @ready + throw new Error('You must specify a `start` function to start loading!') unless opts.start + return this if @loading or @ready @wait() @loading = true @@ -102,11 +112,17 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ # Register a handler for the post-load event that will run only once @once opts.completeEvent, ~> - console.log "#{this}.onLoadComplete()" + # console.log "#{this}.onLoadComplete()" @loading = false @unwait() # terminates the `load` wait @trigger 'load-success', this unless opts.completeEvent is 'load-success' @triggerReady() + @once opts.errorEvent, ~> + # console.log "#{this}.onLoadError()" + @loading = false + @unwait() # terminates the `load` wait + @trigger 'load-error', this unless opts.errorEvent is 'load-error' + @triggerReady() if opts.readyIfError # Finally, start the loading process opts.start.call this diff --git a/lib/chart/chart-option-model.co b/lib/chart/chart-option-model.co deleted file mode 100644 index 2a3c37a..0000000 --- a/lib/chart/chart-option-model.co +++ /dev/null @@ -1,218 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ Parsers, ParserMixin, ParsingModel, ParsingView, -} = require 'kraken/util/parser' -{ BaseModel, BaseList, -} = require 'kraken/base' - - -/** - * @class A set of tags. - */ -class exports.TagSet extends Array - tags : {} - - (values=[]) -> - @tags = {} - @add values if values?.length - - has: (tag) -> - @tags[tag]? - - get: (tag) -> - return -1 unless tag - unless @tags[tag]? - @tags[tag] = @length - @push tag - @tags[tag] - - update: (tags) -> - is_single = typeof tags is 'string' - tags = [tags] if is_single - indices = ( for tag of tags then @get tag ) - if is_single then indices[0] else indices - - toString: -> "TagSet(length=#{@length}, values=[\"#{@join '", "'}\"])" - - - -/** - * @namespace All known tags, for mapping consistently onto colors. - */ -KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet() - - - -/** - * @class Field with chart-option-specific handling for validation, parsing, tags, etc. - */ -ChartOption = exports.ChartOption = ParsingModel.extend do # {{{ - IGNORED_TAGS : <[ callback deprecated debugging ]> - valueAttribute : 'value' - - defaults: -> - name : '' - type : 'String' - default : null - desc : '' - include : 'diff' - tags : [] - examples : [] - - - - constructor: function ChartOption - ParsingModel ... - - initialize : -> - # console.log "#this.initialize!" - - # Bind all the `parseXXX()` methods so they can be passed about independent from the class - _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse')) - - ChartOption.__super__.initialize ... - @set 'id', @id = _.camelize @get 'name' - @set 'value', @get('default'), {+silent} if not @has 'value' - - - # 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() or '' - tags = @get('tags') or [] - if _.str.include(type, 'function') or _.intersection(tags, @IGNORED_TAGS).length - @set 'ignore', true - - - - ### Tag Handling - - # Wrapper to ensure @set('tags') is called, as tags.push() - # will not trigger the 'changed:tags' event. - addTag: (tag) -> - return this unless tag - tags = @get('tags') or [] - tags.push tag - @set 'tags', tags - this - - # Wrapper to ensure @set('tags') is called, as tags.push() - # will not trigger the 'changed:tags' event. - removeTag: (tag) -> - return this unless tag - tags = @get('tags') or [] - _.remove tags, tag - @set 'tags', tags - this - - # Keep tag list up to date - onTagUpdate: -> - KNOWN_TAGS.update @get 'tags' - this - - getTagIndex: (tag) -> - KNOWN_TAGS.get tag - - # A field's category is its first tag. - getCategory: -> - tags = (@get('tags') or [])[0] - - getCategoryIndex: -> - @getTagIndex @getCategory() - - - - /* * * Value Accessors * * */ - - getValue: (def) -> - @getParser() @get @valueAttribute, def - - setValue: (v, options) -> - def = @get 'default' - if not v and def == null - val = null - else - val = @getParser()(v) - @set @valueAttribute, val, options - - clearValue: -> - @set @valueAttribute, @get 'default' - - isDefault: -> - @get(@valueAttribute) is @get 'default' - - - - /* * * Serialization * * */ - - /** - * Override to default `type` to the model attribute of the same name. - * @returns {Function} Parser for the given type. - */ - getParser: (type) -> - type or= @get('type') or 'String' - ChartOption.__super__.getParser.call this, type - - serializeValue: -> - @serialize @getValue() - - toJSON: -> - {id:@id} import do - _.clone(@attributes) import { value:@getValue(), def:@get 'default' } - - toKVPairs: -> - { "#{@id}":@serializeValue() } - - toString: -> "(#{@id}: #{@serializeValue()})" - -# }}} - - - -/** - * @class List of ChartOption fields. - */ -ChartOptionList = exports.ChartOptionList = BaseList.extend do # {{{ - model : ChartOption - - - constructor: function ChartOptionList - BaseList ... - - - /** - * Collects a map of fields to their values, excluding those set to `null` or their default. - * - * @param {Object} [opts={}] Options: - * @param {Boolean} [opts.keepDefaults=true] If false, exclude pairs that - * haven't changed from their default value. - * @param {Boolean} [opts.serialize=false] If true, replace each value - * with its String version by calling `value.serializeValue()`. - * @returns {Object} Map of fields to their values. - */ - values: (opts={}) -> - opts = {+keepDefaults, -serialize} import opts - _.synthesize do - 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 {+keepDefaults, -serialize} - - /** - * Override to omit defaults from URL. - */ - toKVPairs: -> - _.collapseObject @values {-keepDefaults, +serialize} - - toKV: (item_delim='&', kv_delim='=') -> - _.toKV @toKVPairs(), item_delim, kv_delim - - toURL: (item_delim='&', kv_delim='=') -> - "?#{@toKV ...}" - - -# }}} - diff --git a/lib/chart/chart-option-view.co b/lib/chart/chart-option-view.co deleted file mode 100644 index 6c9e2a0..0000000 --- a/lib/chart/chart-option-view.co +++ /dev/null @@ -1,270 +0,0 @@ -{ _, op, -} = require 'kraken/util' -{ BaseView, -} = require 'kraken/base' -{ ChartOption, ChartOptionList, -} = require 'kraken/chart/chart-option-model' - -DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms - - -/** - * @class View for a single configurable option in a chart type. - */ -ChartOptionView = exports.ChartOptionView = BaseView.extend do # {{{ - tagName : 'section' - className : 'chart-option field' - template : require 'kraken/template/chart/chart-option' - - type : 'string' - isCollapsed : true - - events : - 'blur .value' : 'onChange' - 'click input[type="checkbox"].value' : 'onChange' - 'submit .value' : 'onChange' - 'click .close' : 'toggleCollapsed' - 'click h3' : 'toggleCollapsed' - 'click .collapsed' : 'onClick' - - - - constructor: function ChartOptionView - BaseView ... - - initialize: -> - ChartOptionView.__super__.initialize ... - @type = @model.get 'type' .toLowerCase() or 'string' - - - /* * * * Rendering * * * */ - - toTemplateLocals: -> - json = ChartOptionView.__super__.toTemplateLocals ... - json.id or= _.camelize json.name - json.value ?= '' - v = json.value - json.value = JSON.stringify(v) if v and ( _.isArray(v) or _.isPlainObject(v) ) - json - - /** - * Override to annotate with collapsed state and to kill off ignored options - * so they do not contribute their values when looking at form updates. - */ - render: -> - return @remove() if @model.get 'ignore' - ChartOptionView.__super__.render ... - @$el.addClass 'collapsed' if @isCollapsed - this - - - - /* * * * Option Collapsing * * * */ - - /** - * Sets the state of `isCollapsed` and updates the UI. If the state changed, - * a `'change:collapse`` event will be fired.` - * - * @param {Boolean} [makeCollapsed=true] If true, set state to collapsed. - * @returns {Boolean} Whether the state changed. - */ - collapse: (state=true) -> - state = !! state - @isCollapsed = @$el.hasClass 'collapsed' - - return this if state is @isCollapsed - if state - @$el.addClass 'collapsed' - else - @$el.removeClass 'collapsed' - @isCollapsed = state - @trigger 'change:collapse', this, @isCollapsed - true - - /** - * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event. - * @returns {this} - */ - toggleCollapsed: -> - @collapse not @$el.hasClass 'collapsed' - this - - - - /* * * * Events * * * */ - - /** - * To prevent `toggleCollapsed()` from being called multiple times due to - * overlapping listeners, we're only looking for clicks on the collapsed header. - */ - onClick: (evt) -> - target = $ evt.target - @toggleCollapsed() if @$el.hasClass('collapsed') and not target.hasClass('close') - - /** - * Propagate user input changes to the model, and upward to the parent view. - */ - onChange: -> - if @type is 'boolean' - val = !! @$('.value').attr('checked') - else - val = @model.getParser() @$('.value').val() - - current = @model.getValue() - return if _.isEqual val, current - console.log "#this.onChange( #current -> #val )" - @model.setValue val, {+silent} - @trigger 'change', @model, this - # false - - -# }}} - - - -/** - * @class View for configuring a chart type. - */ -ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend do # {{{ - __bind__ : <[ addField ]> - tagName : 'form' - className : 'chart-options scaffold' - template : require 'kraken/template/chart/chart-scaffold' - - collectionType : ChartOptionList - subviewType : ChartOptionView - - events: - 'click .options-filter-button' : 'onFilterOptions' - 'click .collapse-all-options-button' : 'collapseAll' - 'click .expand-all-options-button' : 'expandAll' - - - - - constructor: function ChartOptionScaffold - BaseView ... - - initialize : -> - @render = _.debounce @render.bind(this), DEBOUNCE_RENDER - CollectionType = @collectionType - @model = (@collection or= new CollectionType) - ChartOptionScaffold.__super__.initialize ... - - @collection.on 'add', @addField, this - @collection.on 'reset', @onReset, this - @on 'render', @onRender, this - - - /** - * Bookkeeping for new ChartOptions, creating it a new subview and subscribing - * to its activity, and then rendering it. - * @returns {ChartOptionView} The Option's new view. - */ - addField: (field) -> - @removeSubview field.view if field.view - - # avoid duplicating event propagation - field.off 'change:value', @onChange, this - - # propagate value-change events as key-value change events - field.on 'change:value', @onChange, this - - SubviewType = @subviewType - @addSubview view = new SubviewType model:field - .on 'change', @onChange.bind(this, field) - .on 'change:collapse', @render, this - - @render() # WTF: hmm. - view - - - /* * * * UI * * * */ - - /** - * Collapse all expanded subviews. - * @returns {false} Returns false so event-dispatchers don't propagate - * the triggering event (usually a click or submit). - */ - collapseAll: -> - _.invoke @subviews, 'collapse', true - false - - /** - * Expand all collapsed subviews. - * @returns {false} Returns false so event-dispatchers don't propagate - * the triggering event (usually a click or submit). - */ - expandAll: -> - _.invoke @subviews, 'collapse', false - false - - /** - * Reflow Isotope post-`render()`. - */ - onRender: -> - # console.log "#this.onRender(ready=#{@ready}) -> .isotope()" - - # The DOM doesn't calculate dimensions of elements that are not visible, - # which makes it impossible for Isotope to do its job. - return unless @$el.is ':visible' - - # Invoke Isotope to re-layout the option elements - @$ '.isotope' .isotope do - # itemPositionDataEnabled : true - itemSelector : '.chart-option.field' - layoutMode : 'masonry' - masonry : { columnWidth:10 } - filter : @getOptionsFilter() - sortBy : 'category' - getSortData : - category: ($el) -> - $el.data 'model' .getCategory() - - /** - * @returns {String} Selector representing the selected set of Option filters. - */ - getOptionsFilter: -> - data = @$ '.options-filter-button.active' .toArray().map -> $ it .data() - sel = data.reduce do - (sel, d) -> sel += if d.filter then that else '' - ':not(.ignore)' - sel - - - - /* * * * Events * * * */ - - /** - * Propagate change events from fields as if they were attribute changes. - * Note: `field` is bound to the handler - */ - onChange: (field) -> - key = field.get 'name' - value = field.getValue() - @trigger "change:#key", this, value, key, field - @trigger "change", this, value, key, field - this - - onReset: -> - # The collection has been reset, assume all subviews are - # invalid and rebuild them. - @removeAllSubviews() - @collection.each @addField - _.defer @render - - onFilterOptions: (evt) -> - evt.preventDefault() - # Defer re-rendering until after we yield for the DOM to do its thang - _.defer @render - - -# Proxy collection methods -<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]> - .forEach (methodname) -> - ChartOptionScaffold::[methodname] = -> @collection[methodname].apply @collection, arguments - - -# }}} - - diff --git a/lib/chart/chart-type.co b/lib/chart/chart-type.co index 4da37a1..ad8fabc 100644 --- a/lib/chart/chart-type.co +++ b/lib/chart/chart-type.co @@ -151,7 +151,6 @@ class exports.ChartType extends ReadyEmitter withView : (@view) -> this - /** * Load the corresponding chart specification, which includes * info about valid options, along with their types and defaults. @@ -343,6 +342,28 @@ class exports.ChartType extends ReadyEmitter /** + * Determines chart viewport size. + * @return { width, height } + */ + determineSize: -> + modelW = width = @model.get 'width' + modelH = height = @model.get 'height' + return { width, height } unless @view.ready and width and height + + viewport = @getElementsForRole 'viewport' + + if width is 'auto' + Width = viewport.innerWidth() or 300 + width ?= modelW + + if height is 'auto' + height = viewport.innerHeight() or 320 + height ?= modelH + + { width, height } + + + /** * Transforms domain data and applies it to the chart library to * render or update the corresponding chart. * @@ -360,11 +381,12 @@ class exports.ChartType extends ReadyEmitter * Transforms the domain objects into a hash of derived values using * chart-type-specific keys. * - * @abstract + * Default implementation returns `model.getOptions()`. + * * @returns {Object} The derived data. */ transform: -> - ... + @model.getOptions() /** diff --git a/lib/chart/dygraphs.co b/lib/chart/dygraphs.co deleted file mode 100644 index 49c3f86..0000000 --- a/lib/chart/dygraphs.co +++ /dev/null @@ -1,147 +0,0 @@ -_ = require 'kraken/util/underscore' -{ ChartType, -} = require 'kraken/chart/chart-type' - - -class exports.DygraphsChartType extends ChartType - __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> - SPEC_URL : '/schema/dygraph.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'dygraphs' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - - - /** - * @constructor - */ - -> super ... - - - - ### Formatters {{{ - - # XXX: Dygraphs-specific - makeAxisFormatter: (fmttr) -> - (n, granularity, opts, g) -> fmttr n, opts, g - - # XXX: Dygraphs-specific - dygAxisDateFormatter: (n, granularity, opts, g) -> - moment(n).format 'MM/YYYY' - - # XXX: Dygraphs-specific - dygDateFormatter: (n, opts, g) -> - moment(n).format 'DD MMM YYYY' - - # XXX: Dygraphs-specific - dygNumberFormatter: (n, opts, g) -> - digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2 - { whole, fraction, suffix } = @numberFormatter n, digits - "#whole#fraction#suffix" - - # XXX: Dygraphs-specific - dygNumberFormatterHTML: (n, opts, g) -> - digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2 - # digits = opts('digitsAfterDecimal') ? 2 - { whole, fraction, suffix } = @numberFormatter n, digits - # coco will trim the whitespace - " - #whole - #fraction - #suffix - " - - - ### }}} - ### Rendering {{{ - - /** - * Determines chart viewport size. - * @return { width, height } - */ - determineSize: -> - modelW = width = @model.get 'width' - modelH = height = @model.get 'height' - return { width, height } unless @view.ready and width and height - - viewport = @getElementsForRole 'viewport' - legend = @getElementsForRole 'legend' - - if width is 'auto' - # Remove old style, as it confuses dygraph after options update - delete viewport.prop('style').width - vpWidth = viewport.innerWidth() or 300 - legendW = legend.outerWidth() or 228 - width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW) - width ?= modelW - - if height is 'auto' - # Remove old style, as it confuses dygraph after options update - delete viewport.prop('style').height - height = viewport.innerHeight() or 320 - height ?= modelH - - { width, height } - - - /** - * Resizes the HTML viewport. - */ - resizeViewport: -> - size = @determineSize() - @getElementsForRole 'viewport' .css size - size - - - /** - * Transforms the domain objects into a hash of derived values using - * chart-type-specific keys. - * @returns {Object} The derived chart options. - */ - transform: -> - dataset = @model.dataset - options = @view.chartOptions() import @determineSize() - options import do - colors : dataset.getColors() - labels : dataset.getLabels() - labelsDiv : @getElementsForRole 'legend' .0 - valueFormatter : @dygNumberFormatterHTML - axes: - x: - axisLabelFormatter : @dygAxisDateFormatter - valueFormatter : @dygDateFormatter - y: - axisLabelFormatter : @makeAxisFormatter @dygNumberFormatter - valueFormatter : @dygNumberFormatterHTML - - - /** - * @returns {Dygraph} The Dygraph chart object. - */ - renderChart: (data, viewport, options, lastChart) -> - @resizeViewport() - - # console.log "#this.render!" - # _.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. - lastChart?.destroy() - new Dygraph viewport.0, data, options - - - - ### }}} - - diff --git a/lib/chart/index.co b/lib/chart/index.co index 8cc50ae..5a02eb2 100644 --- a/lib/chart/index.co +++ b/lib/chart/index.co @@ -1,5 +1,5 @@ -chart = require 'kraken/chart/chart-type' -dygraphs = require 'kraken/chart/dygraphs' -models = require 'kraken/chart/chart-option-model' -views = require 'kraken/chart/chart-option-view' -exports import chart import dygraphs import models import views +chart_type = require 'kraken/chart/chart-type' +chart_option = require 'kraken/chart/option' +dygraphs = require 'kraken/chart/type/dygraphs' + +exports import chart_type import chart_option import dygraphs diff --git a/lib/chart/option/chart-option-model.co b/lib/chart/option/chart-option-model.co new file mode 100644 index 0000000..2a3c37a --- /dev/null +++ b/lib/chart/option/chart-option-model.co @@ -0,0 +1,218 @@ +{ _, op, +} = require 'kraken/util' +{ Parsers, ParserMixin, ParsingModel, ParsingView, +} = require 'kraken/util/parser' +{ BaseModel, BaseList, +} = require 'kraken/base' + + +/** + * @class A set of tags. + */ +class exports.TagSet extends Array + tags : {} + + (values=[]) -> + @tags = {} + @add values if values?.length + + has: (tag) -> + @tags[tag]? + + get: (tag) -> + return -1 unless tag + unless @tags[tag]? + @tags[tag] = @length + @push tag + @tags[tag] + + update: (tags) -> + is_single = typeof tags is 'string' + tags = [tags] if is_single + indices = ( for tag of tags then @get tag ) + if is_single then indices[0] else indices + + toString: -> "TagSet(length=#{@length}, values=[\"#{@join '", "'}\"])" + + + +/** + * @namespace All known tags, for mapping consistently onto colors. + */ +KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet() + + + +/** + * @class Field with chart-option-specific handling for validation, parsing, tags, etc. + */ +ChartOption = exports.ChartOption = ParsingModel.extend do # {{{ + IGNORED_TAGS : <[ callback deprecated debugging ]> + valueAttribute : 'value' + + defaults: -> + name : '' + type : 'String' + default : null + desc : '' + include : 'diff' + tags : [] + examples : [] + + + + constructor: function ChartOption + ParsingModel ... + + initialize : -> + # console.log "#this.initialize!" + + # Bind all the `parseXXX()` methods so they can be passed about independent from the class + _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse')) + + ChartOption.__super__.initialize ... + @set 'id', @id = _.camelize @get 'name' + @set 'value', @get('default'), {+silent} if not @has 'value' + + + # 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() or '' + tags = @get('tags') or [] + if _.str.include(type, 'function') or _.intersection(tags, @IGNORED_TAGS).length + @set 'ignore', true + + + + ### Tag Handling + + # Wrapper to ensure @set('tags') is called, as tags.push() + # will not trigger the 'changed:tags' event. + addTag: (tag) -> + return this unless tag + tags = @get('tags') or [] + tags.push tag + @set 'tags', tags + this + + # Wrapper to ensure @set('tags') is called, as tags.push() + # will not trigger the 'changed:tags' event. + removeTag: (tag) -> + return this unless tag + tags = @get('tags') or [] + _.remove tags, tag + @set 'tags', tags + this + + # Keep tag list up to date + onTagUpdate: -> + KNOWN_TAGS.update @get 'tags' + this + + getTagIndex: (tag) -> + KNOWN_TAGS.get tag + + # A field's category is its first tag. + getCategory: -> + tags = (@get('tags') or [])[0] + + getCategoryIndex: -> + @getTagIndex @getCategory() + + + + /* * * Value Accessors * * */ + + getValue: (def) -> + @getParser() @get @valueAttribute, def + + setValue: (v, options) -> + def = @get 'default' + if not v and def == null + val = null + else + val = @getParser()(v) + @set @valueAttribute, val, options + + clearValue: -> + @set @valueAttribute, @get 'default' + + isDefault: -> + @get(@valueAttribute) is @get 'default' + + + + /* * * Serialization * * */ + + /** + * Override to default `type` to the model attribute of the same name. + * @returns {Function} Parser for the given type. + */ + getParser: (type) -> + type or= @get('type') or 'String' + ChartOption.__super__.getParser.call this, type + + serializeValue: -> + @serialize @getValue() + + toJSON: -> + {id:@id} import do + _.clone(@attributes) import { value:@getValue(), def:@get 'default' } + + toKVPairs: -> + { "#{@id}":@serializeValue() } + + toString: -> "(#{@id}: #{@serializeValue()})" + +# }}} + + + +/** + * @class List of ChartOption fields. + */ +ChartOptionList = exports.ChartOptionList = BaseList.extend do # {{{ + model : ChartOption + + + constructor: function ChartOptionList + BaseList ... + + + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepDefaults=true] If false, exclude pairs that + * haven't changed from their default value. + * @param {Boolean} [opts.serialize=false] If true, replace each value + * with its String version by calling `value.serializeValue()`. + * @returns {Object} Map of fields to their values. + */ + values: (opts={}) -> + opts = {+keepDefaults, -serialize} import opts + _.synthesize do + 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 {+keepDefaults, -serialize} + + /** + * Override to omit defaults from URL. + */ + toKVPairs: -> + _.collapseObject @values {-keepDefaults, +serialize} + + toKV: (item_delim='&', kv_delim='=') -> + _.toKV @toKVPairs(), item_delim, kv_delim + + toURL: (item_delim='&', kv_delim='=') -> + "?#{@toKV ...}" + + +# }}} + diff --git a/lib/chart/option/chart-option-view.co b/lib/chart/option/chart-option-view.co new file mode 100644 index 0000000..1b004e1 --- /dev/null +++ b/lib/chart/option/chart-option-view.co @@ -0,0 +1,270 @@ +{ _, op, +} = require 'kraken/util' +{ BaseView, +} = require 'kraken/base' +{ ChartOption, ChartOptionList, +} = require 'kraken/chart/option/chart-option-model' + +DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms + + +/** + * @class View for a single configurable option in a chart type. + */ +ChartOptionView = exports.ChartOptionView = BaseView.extend do # {{{ + tagName : 'section' + className : 'chart-option field' + template : require 'kraken/template/chart/chart-option' + + type : 'string' + isCollapsed : true + + events : + 'blur .value' : 'onChange' + 'click input[type="checkbox"].value' : 'onChange' + 'submit .value' : 'onChange' + 'click .close' : 'toggleCollapsed' + 'click h3' : 'toggleCollapsed' + 'click .collapsed' : 'onClick' + + + + constructor: function ChartOptionView + BaseView ... + + initialize: -> + ChartOptionView.__super__.initialize ... + @type = @model.get 'type' .toLowerCase() or 'string' + + + /* * * * Rendering * * * */ + + toTemplateLocals: -> + json = ChartOptionView.__super__.toTemplateLocals ... + json.id or= _.camelize json.name + json.value ?= '' + v = json.value + json.value = JSON.stringify(v) if v and ( _.isArray(v) or _.isPlainObject(v) ) + json + + /** + * Override to annotate with collapsed state and to kill off ignored options + * so they do not contribute their values when looking at form updates. + */ + render: -> + return @remove() if @model.get 'ignore' + ChartOptionView.__super__.render ... + @$el.addClass 'collapsed' if @isCollapsed + this + + + + /* * * * Option Collapsing * * * */ + + /** + * Sets the state of `isCollapsed` and updates the UI. If the state changed, + * a `'change:collapse`` event will be fired.` + * + * @param {Boolean} [makeCollapsed=true] If true, set state to collapsed. + * @returns {Boolean} Whether the state changed. + */ + collapse: (state=true) -> + state = !! state + @isCollapsed = @$el.hasClass 'collapsed' + + return this if state is @isCollapsed + if state + @$el.addClass 'collapsed' + else + @$el.removeClass 'collapsed' + @isCollapsed = state + @trigger 'change:collapse', this, @isCollapsed + true + + /** + * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event. + * @returns {this} + */ + toggleCollapsed: -> + @collapse not @$el.hasClass 'collapsed' + this + + + + /* * * * Events * * * */ + + /** + * To prevent `toggleCollapsed()` from being called multiple times due to + * overlapping listeners, we're only looking for clicks on the collapsed header. + */ + onClick: (evt) -> + target = $ evt.target + @toggleCollapsed() if @$el.hasClass('collapsed') and not target.hasClass('close') + + /** + * Propagate user input changes to the model, and upward to the parent view. + */ + onChange: -> + if @type is 'boolean' + val = !! @$('.value').attr('checked') + else + val = @model.getParser() @$('.value').val() + + current = @model.getValue() + return if _.isEqual val, current + console.log "#this.onChange( #current -> #val )" + @model.setValue val, {+silent} + @trigger 'change', @model, this + # false + + +# }}} + + + +/** + * @class View for configuring a chart type. + */ +ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend do # {{{ + __bind__ : <[ addField ]> + tagName : 'form' + className : 'chart-options scaffold' + template : require 'kraken/template/chart/chart-scaffold' + + collectionType : ChartOptionList + subviewType : ChartOptionView + + events: + 'click .options-filter-button' : 'onFilterOptions' + 'click .collapse-all-options-button' : 'collapseAll' + 'click .expand-all-options-button' : 'expandAll' + + + + + constructor: function ChartOptionScaffold + BaseView ... + + initialize : -> + @render = _.debounce @render.bind(this), DEBOUNCE_RENDER + CollectionType = @collectionType + @model = (@collection or= new CollectionType) + ChartOptionScaffold.__super__.initialize ... + + @collection.on 'add', @addField, this + @collection.on 'reset', @onReset, this + @on 'render', @onRender, this + + + /** + * Bookkeeping for new ChartOptions, creating it a new subview and subscribing + * to its activity, and then rendering it. + * @returns {ChartOptionView} The Option's new view. + */ + addField: (field) -> + @removeSubview field.view if field.view + + # avoid duplicating event propagation + field.off 'change:value', @onChange, this + + # propagate value-change events as key-value change events + field.on 'change:value', @onChange, this + + SubviewType = @subviewType + @addSubview view = new SubviewType model:field + .on 'change', @onChange.bind(this, field) + .on 'change:collapse', @render, this + + @render() # WTF: hmm. + view + + + /* * * * UI * * * */ + + /** + * Collapse all expanded subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */ + collapseAll: -> + _.invoke @subviews, 'collapse', true + false + + /** + * Expand all collapsed subviews. + * @returns {false} Returns false so event-dispatchers don't propagate + * the triggering event (usually a click or submit). + */ + expandAll: -> + _.invoke @subviews, 'collapse', false + false + + /** + * Reflow Isotope post-`render()`. + */ + onRender: -> + # console.log "#this.onRender(ready=#{@ready}) -> .isotope()" + + # The DOM doesn't calculate dimensions of elements that are not visible, + # which makes it impossible for Isotope to do its job. + return unless @$el.is ':visible' + + # Invoke Isotope to re-layout the option elements + @$ '.isotope' .isotope do + # itemPositionDataEnabled : true + itemSelector : '.chart-option.field' + layoutMode : 'masonry' + masonry : { columnWidth:10 } + filter : @getOptionsFilter() + sortBy : 'category' + getSortData : + category: ($el) -> + $el.data 'model' .getCategory() + + /** + * @returns {String} Selector representing the selected set of Option filters. + */ + getOptionsFilter: -> + data = @$ '.options-filter-button.active' .toArray().map -> $ it .data() + sel = data.reduce do + (sel, d) -> sel += if d.filter then that else '' + ':not(.ignore)' + sel + + + + /* * * * Events * * * */ + + /** + * Propagate change events from fields as if they were attribute changes. + * Note: `field` is bound to the handler + */ + onChange: (field) -> + key = field.get 'name' + value = field.getValue() + @trigger "change:#key", this, value, key, field + @trigger "change", this, value, key, field + this + + onReset: -> + # The collection has been reset, assume all subviews are + # invalid and rebuild them. + @removeAllSubviews() + @collection.each @addField + _.defer @render + + onFilterOptions: (evt) -> + evt.preventDefault() + # Defer re-rendering until after we yield for the DOM to do its thang + _.defer @render + + +# Proxy collection methods +<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]> + .forEach (methodname) -> + ChartOptionScaffold::[methodname] = -> @collection[methodname].apply @collection, arguments + + +# }}} + + diff --git a/lib/chart/option/index.co b/lib/chart/option/index.co new file mode 100644 index 0000000..5cdea4d --- /dev/null +++ b/lib/chart/option/index.co @@ -0,0 +1,4 @@ +model = require 'kraken/chart/option/chart-option-model' +view = require 'kraken/chart/option/chart-option-view' + +exports import model import view diff --git a/lib/chart/type/d3-bar.co b/lib/chart/type/d3-bar.co deleted file mode 100644 index e69de29..0000000 diff --git a/lib/chart/type/d3-geo.co b/lib/chart/type/d3-geo.co deleted file mode 100644 index e69de29..0000000 diff --git a/lib/chart/type/d3-line.co b/lib/chart/type/d3-line.co deleted file mode 100644 index e69de29..0000000 diff --git a/lib/chart/type/d3/d3-bar-chart-type.co b/lib/chart/type/d3/d3-bar-chart-type.co new file mode 100644 index 0000000..e69de29 diff --git a/lib/chart/type/d3/d3-geo-chart-type.co b/lib/chart/type/d3/d3-geo-chart-type.co new file mode 100644 index 0000000..4709aa0 --- /dev/null +++ b/lib/chart/type/d3/d3-geo-chart-type.co @@ -0,0 +1,182 @@ +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ ChartType, +} = require 'kraken/chart' + +class GeoWorldChartType extends ChartType + __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> + SPEC_URL : '/schema/d3/d3-geo-world.json' + + # NOTE: ChartType.register() must come AFTER `typeName` declaration. + typeName : 'd3-geo-world' + ChartType.register this + + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + legend : '.graph-legend' + + + + -> super ... + + + transform: -> + options = @model.getOptions() import @determineSize() + # options.colors.palette = ["black", "red"] if options.colors.palette? + options.colors.scaleDomain = d3.extent if options.colors.scaleDomain? + options + + + getProjection : (type) -> + switch type + case 'mercator' 'albers' 'albersUsa' + d3.geo[type]() + case 'azimuthalOrtho' + d3.geo.azimuthal() + .mode 'orthographic' + case 'azimuthalStereo' + d3.geo.azimuthal() + .mode 'stereographic' + default + throw new Error "Invalid map projection type '#type'!" + + + renderChart: (data, viewport, options, lastChart) -> + {width, height} = options + + fill = @fill = (data, options) -> + d3.scale[ options.colors.scale ]() + .domain options.colors.scaleDomain + .range options.colors.palette + + quantize = @quantize = (data, options) -> + (d) -> + if data[d.properties.name]? + return fill data[d.properties.name].editors + else + # console.log 'Country '+d.properties.name+' not in data' + return fill "rgb(0,0,0)" + + projection = @projection = @getProjection(options.map.projection) + .scale width + .translate [width/2, height/2] + + path = d3.geo.path() + .projection projection + + move = -> + projection + .translate d3.event.translate + .scale d3.event.scale + feature.attr "d", path + + zoom = d3.behavior.zoom() + .translate projection.translate() + .scale projection.scale() + .scaleExtent [height,height*8] + .on "zoom", move + + + #### + + chart = d3.select viewport.0 + .append "svg:svg" + .attr "width", width + .attr "height", height + .append "svg:g" + .attr "transform", "translate(0,0)" + .call zoom + + # path objects + feature := map.selectAll ".feature" + + # rectangle + map.append "svg:rect" + .attr "class", "frame" + .attr "width", width + .attr "height", height + + + ### infobox + infobox := d3.select '#infobox' + + infobox.select '#ball' + .append "svg:svg" + .attr "width", "100%" + .attr "height", "20px" + .append "svg:rect" + .attr "width", "60%" + .attr "height", "20px" + .attr "fill", '#f40500' + + setInfoBox = (d) -> + name = d.properties.name + ae = 0 + e5 = 0 + e100 = 0 + + if data[name]? + ae = parseInt data[name].editors + e5 = parseInt data[name].editors5 + e100 = parseInt data[name].editors100 + + infobox.select '#country' .text name + infobox.select '#ae' .text ae + infobox.select '#e5' .text e5+" ("+(100.0*e5/ae).toPrecision(3)+"%)" + infobox.select '#e100' .text e100+" ("+(100.0*e100/ae).toPrecision(3)+"%)" + + xy = d3.svg.mouse this + infobox.style "left", xy[0]+'px' + infobox.style "top", xy[1]+'px' + infobox.style "display", "block" + + + worldmap = -> + d3.json do + "/data/geo/maps/world-countries.json" + (json) -> + feature := feature + .data json.features + .enter().append "svg:path" + .attr "class", "feature" + .attr "d", path + .attr "fill", quantize + .attr "id", (d) -> d.properties.name + .on "mouseover", setInfoBox + .on "mouseout", -> infobox.style "display", "none" + + + + + + +main = -> + jQuery.ajax do + url : "/data/geo/data/en_geo_editors.json" + dataType : 'json' + success : (res) -> + # result will be the returned JSON + data := res + + # delete & hide spinner + jQuery '.geo-spinner' .spin(false).hide() + + # load the world map + worldmap() + + # adding bootstrap tooltips + # $ '.page-header' .tooltip title:"for the header it works but is useless" + # $ '.feature' .tooltip title:"here it doesn't work" + + console.log 'Loaded geo coding map!' + error : (err) -> console.error err + + diff --git a/lib/chart/type/d3/d3-line-chart-type.co b/lib/chart/type/d3/d3-line-chart-type.co new file mode 100644 index 0000000..e69de29 diff --git a/lib/chart/type/d3/index.co b/lib/chart/type/d3/index.co new file mode 100644 index 0000000..3a488eb --- /dev/null +++ b/lib/chart/type/d3/index.co @@ -0,0 +1,5 @@ +bar = require 'kraken/chart/type/d3/d3-bar-chart-type' +geo = require 'kraken/chart/type/d3/d3-geo-chart-type' +line = require 'kraken/chart/type/d3/d3-line-chart-type' + +exports import bar import geo import line diff --git a/lib/chart/type/dygraphs.co b/lib/chart/type/dygraphs.co new file mode 100644 index 0000000..7ae8e67 --- /dev/null +++ b/lib/chart/type/dygraphs.co @@ -0,0 +1,142 @@ +_ = require 'kraken/util/underscore' +{ ChartType, +} = require 'kraken/chart/chart-type' + + +class exports.DygraphsChartType extends ChartType + __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> + SPEC_URL : '/schema/dygraph.json' + + # NOTE: ChartType.register() must come AFTER `typeName` declaration. + typeName : 'dygraphs' + ChartType.register this + + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + legend : '.graph-legend' + + -> super ... + + + + ### Formatters {{{ + + # XXX: Dygraphs-specific + makeAxisFormatter: (fmttr) -> + (n, granularity, opts, g) -> fmttr n, opts, g + + # XXX: Dygraphs-specific + dygAxisDateFormatter: (n, granularity, opts, g) -> + moment(n).format 'MM/YYYY' + + # XXX: Dygraphs-specific + dygDateFormatter: (n, opts, g) -> + moment(n).format 'DD MMM YYYY' + + # XXX: Dygraphs-specific + dygNumberFormatter: (n, opts, g) -> + digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2 + { whole, fraction, suffix } = @numberFormatter n, digits + "#whole#fraction#suffix" + + # XXX: Dygraphs-specific + dygNumberFormatterHTML: (n, opts, g) -> + digits = if typeof opts('digitsAfterDecimal') is 'number' then that else 2 + # digits = opts('digitsAfterDecimal') ? 2 + { whole, fraction, suffix } = @numberFormatter n, digits + # coco will trim the whitespace + " + #whole + #fraction + #suffix + " + + + ### }}} + ### Rendering {{{ + + /** + * Determines chart viewport size. + * @return { width, height } + */ + determineSize: -> + modelW = width = @model.get 'width' + modelH = height = @model.get 'height' + return { width, height } unless @view.ready and width and height + + viewport = @getElementsForRole 'viewport' + legend = @getElementsForRole 'legend' + + if width is 'auto' + # Remove old style, as it confuses dygraph after options update + delete viewport.prop('style').width + vpWidth = viewport.innerWidth() or 300 + legendW = legend.outerWidth() or 228 + width = vpWidth - legendW - 10 - (vpWidth - legend.position().left - legendW) + width ?= modelW + + if height is 'auto' + # Remove old style, as it confuses dygraph after options update + delete viewport.prop('style').height + height = viewport.innerHeight() or 320 + height ?= modelH + + { width, height } + + + /** + * Resizes the HTML viewport. + */ + resizeViewport: -> + size = @determineSize() + @getElementsForRole 'viewport' .css size + size + + + /** + * Transforms the domain objects into a hash of derived values using + * chart-type-specific keys. + * @returns {Object} The derived chart options. + */ + transform: -> + dataset = @model.dataset + options = @view.chartOptions() import @determineSize() + options import do + colors : dataset.getColors() + labels : dataset.getLabels() + labelsDiv : @getElementsForRole 'legend' .0 + valueFormatter : @dygNumberFormatterHTML + axes: + x: + axisLabelFormatter : @dygAxisDateFormatter + valueFormatter : @dygDateFormatter + y: + axisLabelFormatter : @makeAxisFormatter @dygNumberFormatter + valueFormatter : @dygNumberFormatterHTML + + + /** + * @returns {Dygraph} The Dygraph chart object. + */ + renderChart: (data, viewport, options, lastChart) -> + @resizeViewport() + + # console.log "#this.render!" + # _.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. + lastChart?.destroy() + new Dygraph viewport.0, data, options + + + + ### }}} + + diff --git a/lib/graph/graph-model.co b/lib/graph/graph-model.co index 385ad7b..c5b59ce 100644 --- a/lib/graph/graph-model.co +++ b/lib/graph/graph-model.co @@ -60,19 +60,17 @@ Graph = exports.Graph = BaseModel.extend do # {{{ * Attribute defaults. */ defaults: -> - { - slug : '' - name : '' - desc : '' - notes : '' - # dataset : '/data/datasources/rc/rc_comscore_region_uv.csv' - # dataset : null - width : 'auto' - height : 320 - chartType : 'dygraphs' - parents : <[ root ]> - options : {} - } + slug : '' + name : '' + desc : '' + notes : '' + # dataset : '/data/datasources/rc/rc_comscore_region_uv.csv' + # dataset : null + width : 'auto' + height : 320 + chartType : 'dygraphs' + parents : <[ root ]> + options : {} url: -> "#{@urlRoot}/#{@get('slug')}.json" diff --git a/lib/main-edit.co b/lib/main-edit.co index 18e21cb..0ccad34 100644 --- a/lib/main-edit.co +++ b/lib/main-edit.co @@ -1,8 +1,12 @@ +{EventEmitter} = require 'events' + Seq = require 'seq' Backbone = require 'backbone' { _, op, } = require 'kraken/util' +{ AppView, +} = require 'kraken/app' { BaseView, BaseModel, BaseList, } = require 'kraken/base' { ChartType, @@ -49,12 +53,10 @@ main = -> # _.dump _.clone(data.options), 'data.options' - # Instantiate model & view - graph = root.graph = new Graph data, {+parse} - view = root.view = new GraphEditView do - model : graph - - $ '#content .inner' .append view.el + # Instantiate app with model & view + root.app = new AppView -> + @model = root.graph = new Graph data, {+parse} + @view = root.view = new GraphEditView {@model} # Load data files diff --git a/lib/server/server.co b/lib/server/server.co index 3dfc9ad..f7a4fc2 100755 --- a/lib/server/server.co +++ b/lib/server/server.co @@ -118,8 +118,9 @@ app.configure -> log_level : LOG_LEVEL app.use require('browserify') do mount : '/vendor/browserify.js' - require : <[ events seq ]> - cache : "#CWD/.cache/browserify/cache.json" + require : <[ seq events ]> + cache : false + # cache : "#CWD/.cache/browserify/cache.json" # Serve static files app.use express.static WWW diff --git a/msc/list-option-tags.py b/msc/list-option-tags.py new file mode 100755 index 0000000..c0384f5 --- /dev/null +++ b/msc/list-option-tags.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" List the tags found in the option-schema file. +""" +__author__ = 'David Schoonover ' + +import sys, re, argparse +from lessly import Script +import yaml + + +class ListOptionTags(Script): + + def __call__(self): + self.optionSchema = yaml.load(self.infile) + + self.tags = [] + for opt in self.optionSchema: + self.tags.extend( opt.get('tags', []) ) + self.tags = set(self.tags) + + self.outfile.write( '\n'.join(sorted(self.tags)) + '\n' ) + + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('infile', nargs='?', type=argparse.FileType('rU'), default=sys.stdin) + parser.add_argument('outfile', nargs='?', type=argparse.FileType('w'), default=sys.stdout) + + +if __name__ == '__main__': + sys.exit(ListOptionTags.main() or 0) + diff --git a/package.co b/package.co index a341841..3b76b1c 100644 --- a/package.co +++ b/package.co @@ -34,7 +34,7 @@ devDependencies : 'uglify-js' : '>= 1.2.6' scripts : test:'expresso' -repository : type:'git', url:'git://git@less.ly:kraken-ui.git' +repository : type:'git', url:'git://less.ly/kraken-ui.git' engine : node:'>=0.6.2' license : 'MIT' diff --git a/www/css/geo-display.styl b/www/css/geo-display.styl index d4c20cc..b3195b3 100644 --- a/www/css/geo-display.styl +++ b/www/css/geo-display.styl @@ -39,7 +39,7 @@ section.geo // max-width 900px .frame - stroke #000 + stroke #333 fill none pointer-events all diff --git a/www/modules.yaml b/www/modules.yaml index aba31e8..8e40043 100644 --- a/www/modules.yaml +++ b/www/modules.yaml @@ -103,10 +103,14 @@ dev: - graph-list-view - index - chart: + - option: + - chart-option-model + - chart-option-view + - index + - type: + - dygraphs + - index - chart-type - - dygraphs - - chart-option-view - - chart-option-model - index - data: - metric-model @@ -122,6 +126,7 @@ dev: - dashboard-model - dashboard-view - index + - app # - suffix: .js # paths: diff --git a/www/schema/d3/d3-geo-world.yaml b/www/schema/d3/d3-geo-world.yaml new file mode 100644 index 0000000..03325c8 --- /dev/null +++ b/www/schema/d3/d3-geo-world.yaml @@ -0,0 +1,99 @@ +- name: zoom.start + tags: + - interactivity + - zoom + type: Float + default: 1.0 + desc: Initial zoom-level, where 1.0 (100% zoom) shows the full map in the + frame (the default). + +- name: zoom.min + tags: + - interactivity + - zoom + type: Float + default: 1.0 + desc: Limit to the amount the chart will zoom out, expressed as a multiplier + of the frame. By default, this is limited to show the whole map in the + frame. + +- name: zoom.max + tags: + - interactivity + - zoom + type: Float + default: 8.0 + desc: Limit to the amount the chart will zoom in, expressed as a multiplier + of the frame (8x by default). + +- name: colors.palette + tags: + - color + - axes + - standard + type: Array + default: [black, red] + desc: Array of colors to which values are mapped (based on their position + in `colors.scaleDomain`). + +- name: colors.scale + tags: + - color + - axes + - standard + type: enum + values: + - linear + - log + default: log + desc: Scale color differences in the map using a scale-transform (log-scale by + default). Options include: + - Linear scaling + - Logarithmic scaling + +- name: colors.scaleDomain + tags: + - color + - axes + type: Array + default: null + desc: Domain for scaling color differences. Uses the extent of the dataset + by default (and when `null`), meaning the smallest value will map to the first + color of the palette, and the largest value to the last color. + +- name: colors.missing + tags: + - standard + - color + - data + type: String + default: 'rgba(0,0,0,0)' + desc: Features without values are replaced with this color (transparent by default). + +- name: map.projection + tags: + - geo + - map + type: enum + values: + - mercator + - albers + - albersUsa + - azimuthalOrtho + - azimuthalStereo + default: mercator + desc: Projection for map-data (mercator by default). Options include: + - Spherical mercator projection + - Albers equal-area conic projection + - Composite Albers projection for the United States + - Orthographic Azimuthal projection + - Stereographic Azimuthal projection + +- name: map.definition + tags: + - geo + - map + type: String + default: "/data/geo/maps/world-countries.json" + desc: Path or URL to the `geoJSON` map definition data. + -- 1.7.0.4