From: dsc Date: Tue, 10 Apr 2012 00:45:30 +0000 (-0700) Subject: Adds GraphDisplayView X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=7a752ef4455f80b27aac6bdd2be6d00a51b07c93;p=limn-bak.git Adds GraphDisplayView --- diff --git a/lib/graph/graph-display-view.co b/lib/graph/graph-display-view.co index cdbdfd3..11ac226 100644 --- a/lib/graph/graph-display-view.co +++ b/lib/graph/graph-display-view.co @@ -1,33 +1,268 @@ -root = do -> this +moment = require 'moment' -_ = require 'kraken/util/underscore' +{ _, op, +} = require 'kraken/util' { BaseView, } = require 'kraken/base' -{ ChartType, DEBOUNCE_RENDER, -} = require 'kraken/chart' { Graph, } = require 'kraken/graph/graph-model' +root = do -> this +DEBOUNCE_RENDER = 100ms + +/** + * @class View for a graph visualization encapsulating. + */ GraphDisplayView = exports.GraphDisplayView = BaseView.extend do # {{{ - ctorName : 'GraphDisplayView' + FILTER_CHART_OPTIONS : <[ + file labels visibility colors dateWindow ticker timingName xValueParser + axisLabelFormatter xAxisLabelFormatter yAxisLabelFormatter + valueFormatter xValueFormatter yValueFormatter + ]> + __bind__ : <[ + render stopAndRender resizeViewport + numberFormatter numberFormatterHTML + onReady onSync onModelChange + ]> + __debounce__: <[ render ]> + tagName : 'section' - className : 'graph' + className : 'graph graph-display' template : require 'kraken/template/graph-display' - # events : - # 'blur .value' : 'update' - # 'submit .value' : 'update' + # events: + # 'click .redraw-button' : 'stopAndRender' + # 'click .load-button' : 'load' + + data : {} + ready : false constructor: function GraphDisplayView BaseView ... - initialize: -> + initialize : (o={}) -> + @data = {} @model or= new Graph + @id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid) BaseView::initialize ... + # console.log "#this.initialize!" + + for name of @__debounce__ + @[name] = _.debounce @[name], DEBOUNCE_RENDER + + @viewport = @$el.find '.viewport' + + ### Model Events + @model + .on 'ready', @onReady, this + .on 'sync', @onSync, this + .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 + + @chartOptions @model.getOptions(), {+silent} + + ### Chart Viewport + @resizeViewport() + + # Resize chart on window resize + # Note: can't debounce the method itself, as the debounce wrapper returns undefined + $ root .on 'resize', _.debounce @resizeViewport, DEBOUNCE_RENDER + + + + + load: -> + console.log "#this.load!" + @model.fetch() + false + + change: -> + @model.change() + this + + chartOptions: (values, opts) -> + # Handle @chartOptions(k, v, opts) + if arguments.length > 1 and typeof values is 'string' + [k, v, opts] = arguments + values = { "#k": v } + + 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] + delete options[k] + options + + + toTemplateLocals: -> + attrs = _.clone @model.attributes + delete attrs.options + # delete attrs.dataset + attrs.data = @data + { $, _, op, @model, view:this } import attrs + + + /** + * Resizes chart according to the model's width and height. + * @return { width, height } + */ + resizeViewport: -> + modelW = width = @model.get 'width' + modelH = height = @model.get 'height' + return { width, height } unless @ready + + # Remove old style, as it confuses dygraph after options update + @viewport.attr 'style', '' + label = @$el.find '.graph-legend' + + if width is 'auto' + vpWidth = @viewport.innerWidth() + labelW = label.outerWidth() + width = vpWidth - labelW - 10 - (vpWidth - label.position().left - labelW) + width ?= modelW + if height is 'auto' + height = @viewport.innerHeight() + height ?= modelH + + size = { width, height } + @viewport.css size + # console.log 'resizeViewport!', JSON.stringify(size), @viewport + # @chart.resize size if forceRedraw + size + + + # Redraw chart inside viewport. + renderChart: -> + data = @model.get 'dataset' #.getData() + size = @resizeViewport() + + # XXX: use @model.changedAttributes() to calculate what to update + options = @chartOptions() #import size + options import do + labelsDiv : @$el.find '.graph-legend' .0 + valueFormatter : @numberFormatterHTML + axes: + x: + axisLabelFormatter : @axisDateFormatter + valueFormatter : @dateFormatter + y: + axisLabelFormatter : @axisFormatter @numberFormatter + valueFormatter : @numberFormatterHTML + + # console.log "#this.render!", dataset + _.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. + @chart?.destroy() + @chart = new Dygraph do + @viewport.0 + data + options + + # unless @chart + # @chart = new Dygraph do + # @viewport.0 + # data + # options + # else + # @chart.updateOptions options + # @chart.resize size + + this + + update: -> + locals = @toTemplateLocals() + @$el.find '.graph-name' .text(locals.name or '') + @$el.find '.graph-desc' .html jade.filters.markdown locals.desc or '' + this + + render: -> + return this unless @ready + root.title = "#{@get 'name'} | GraphKit" + @update() + _.invoke @subviews, 'render' + @renderChart() + @trigger 'render', this + false - toString: -> "#{@ctorName}(#{@model})" + + + ### Formatters {{{ + + axisFormatter: (fmttr) -> + (n, granularity, opts, g) -> fmttr n, opts, g + + axisDateFormatter: (n, granularity, opts, g) -> + moment(n).format 'MM/YYYY' + + dateFormatter: (n, opts, g) -> + moment(n).format 'DD MMM YYYY' + + _numberFormatter: (n, digits=2) -> + for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + if n >= d + n = n / d + break + s = n.toFixed(digits) + parts = s.split '.' + whole = _.rchop parts[0], 3 .join ',' + fraction = '.' + parts.slice(1).join '.' + { n, digits, whole, fraction, suffix } + + numberFormatter: (n, opts, g) -> + digits = opts('digitsAfterDecimal') ? 2 + { whole, fraction, suffix } = @_numberFormatter n, digits + "#whole#fraction#suffix" + + numberFormatterHTML: (n, opts, g) -> + digits = opts('digitsAfterDecimal') ? 2 + { whole, fraction, suffix } = @_numberFormatter n, digits + """ + #whole#fraction#suffix + """ + + ### }}} + ### Event Handlers {{{ + + + onReady: -> + return if @ready + $.getJSON '/datasources/all', (@data) ~> + console.log "(#this via GraphDisplayView).ready!" + @ready = true + @onSync() + + onSync: -> + return unless @ready + console.info "#this.sync() --> success!" + # TODO: UI alert + # @change() + # @model.change() + @render() + + onModelChange: -> + changes = @model.changedAttributes() + 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 + + + # Needed because (sigh) _.debounce returns undefined + stopAndRender: -> + @render ... + false + + + # }}} # }}} + diff --git a/lib/graph/graph-model.co b/lib/graph/graph-model.co index 62dec85..0e8c209 100644 --- a/lib/graph/graph-model.co +++ b/lib/graph/graph-model.co @@ -59,6 +59,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{ slug : '' name : '' desc : '' + notes : '' dataset : '/data/datasources/rc/rc_comscore_region_uv.csv' # dataset : null width : 'auto' diff --git a/lib/main.co b/lib/main-display.co similarity index 88% copy from lib/main.co copy to lib/main-display.co index 61d2da9..1d1781f 100644 --- a/lib/main.co +++ b/lib/main-display.co @@ -5,13 +5,9 @@ Backbone = require 'backbone' } = require 'kraken/util' { BaseView, BaseModel, BaseList, } = require 'kraken/base' -{ Field, FieldList, FieldView, Scaffold, -} = require 'kraken/scaffold' { ChartType, DygraphsChartType, - ChartOption, ChartOptionList, TagSet, - ChartOptionView, ChartOptionScaffold, } = require 'kraken/chart' -{ Graph, GraphList, GraphEditView, +{ Graph, GraphList, GraphDisplayView, } = require 'kraken/graph' @@ -52,7 +48,7 @@ main = -> # Instantiate model & view graph = root.graph = new Graph data, {+parse} - view = root.view = new GraphEditView do + view = root.view = new GraphDisplayView do graph_spec : root.CHART_OPTIONS_SPEC # FIXME: necessary? model : graph diff --git a/lib/main.co b/lib/main-edit.co similarity index 100% rename from lib/main.co rename to lib/main-edit.co diff --git a/lib/template/graph-display.jade b/lib/template/graph-display.jade index 74e403e..a593597 100644 --- a/lib/template/graph-display.jade +++ b/lib/template/graph-display.jade @@ -1,59 +1,18 @@ -- var id = model.id || model.cid +include browser-helpers - var graph_id = view.id -section.graph(id=graph_id) - form.details.form-horizontal +section.graph.graph-display(id=view.id) - .name-row.row-fluid.control-group - //- label.name.control-label(for="#{id}_name"): h3 Graph Name - input.span6.name(type='text', id="#{id}_name", name="name", placeholder='Graph Name', value=name) + .graph-name-row.page-header.row-fluid + h1.graph-name #{name} - .row-fluid + .graph-viewport-row.row-fluid .viewport - .graph-label + .graph-legend - .row-fluid - .graph-settings.tabbable - //- nav.navbar: div.navbar-inner: div.container - nav - ul.nav.subnav.nav-pills - li: h3 Graph - li.active: a(href="#graph-#{graph_id}-info", data-toggle="tab") Info - li: a(href="#graph-#{graph_id}-data", data-toggle="tab") Data - li: a.graph-options-tab(href="#graph-#{graph_id}-options", data-toggle="tab") Options - li.graph-controls - input.redraw-button.btn(type="button", value="Redraw") - input.load-button.btn(type="button", value="Load") - input.save-button.btn-success(type="button", value="Save") - - .tab-content - .graph-info-pane.tab-pane.active(id="graph-#{graph_id}-info") - .row-fluid - .half.control-group - .control-group - label.slug.control-label(for='slug') Slug - .controls - 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. - .control-group - label.width.control-label(for='width') Size - .controls - input.span1.width(type='text', id='width', name='width', value=width) - | × - input.span1.height(type='text', 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 - .controls - //- textarea.span3.desc(id='desc', name='desc', placeholder='Graph description.') #{desc} - - 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 - .controls - 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. - - .graph-options-pane.tab-pane(id="graph-#{graph_id}-options") - + .graph-details-row.row-fluid + .graph-desc + != jade.filters.markdown(desc) + + //- + .graph-notes.span6 + != jade.filters.markdown(notes) diff --git a/www/css/graph-display.styl b/www/css/graph-display.styl new file mode 100644 index 0000000..29568bd --- /dev/null +++ b/www/css/graph-display.styl @@ -0,0 +1,249 @@ +@import 'colors' +@import 'nib' + +section.graph.graph-display + position relative + max-width 900px + margin 0 auto + + * + position relative + + /* * * * Chart & Viewport * * * {{{ */ + .graph-legend + position absolute + z-index 100 + top 1em + right 1em + width 200px + + padding 1em + background-color rgba(255,255,255, 0.75) + font 12px/1.5 "helvetica neue", helvetica, arial, sans-serif + border 1px solid $light + border-radius 5px + + b + display inline-block + width 140px + .whole + display inline-block + width 30px + text-align right + .fraction + text-align left + .suffix + text-align left + + .viewport:hover + .graph-legend + border 1px solid $light + + .viewport + position relative + min-width 200px + min-height 320px + margin-bottom 1.5em + overflow hidden + + /* }}} */ + + + /* * * * Subnav & Tabs * * * {{{ */ + .graph-settings.tabbable + .nav + margin-bottom 0 + + li h3 + line-height 14px + margin 2px + padding 8px 12px + border-radius 5px + li + margin-right 4px + + .tab-pane + padding 0.5em + margin-top 18px + + &.graph-data-pane + margin-top 0 + padding 0 + + .graph-controls + z-index 100 + margin-top 2px + + & > .btn, & > .btn-group + margin 0 0.5em + + .btn-group + display inline-block + + input[type="button"] + min-width 5em + text-align center + + /* }}} */ + + + /* * * * Graph Details * * * {{{ */ + form.details + position relative + + .name-row + font-size 120% + line-height 2em + input.name + font-size 120% + line-height 1.2 + height 1.2em + border-color $light + width 98% + .row-fluid + .half.control-group + width 50% + float left + margin-left 0 + margin-right 0 + label + width 100px + .controls + margin-left 110px + .help-block + font-size 11px + line-height 1.3 + /* }}} */ + + + /* * * * Chart Options * * * {{{ */ + .options fieldset + border 0px + + .field.option + float left + z-index 3 + padding 0.5em + margin 0.4em + min-width 200px + max-width 250px + min-height 1.5em + line-height 1.5 + overflow hidden + + border-radius 5px + background-color #ccc + font-size 90% + + + h3 + font-size 14px + line-height 1.3 + cursor pointer + + .close + absolute top right 0.1em + width 1em + height 1em + line-height 1.2em + text-align center + text-decoration none + z-index 10 + cursor pointer + opacity 0.3 + &:hover + opacity 0.6 + + .shortname + font-weight bold + color white + min-height 1.5em + .name + display none + font-weight bold + // line-height 1.5 + // font-size 100% + input.value:not([type="checkbox"]) + width 240px + font-family menlo, monospace + .type + &::before + content "Type: " + font-weight bold + .default + &::before + content "Default: " + font-weight bold + .desc + position relative + .tags, .examples + cursor pointer + .tags + font-size 85% + &::before + content "Tags: " + font-weight bold + .tag + margin 0.2em + line-height 1.5 + padding 0.2em + white-space nowrap + color white + background-color rgba(255,255,255, 0.15) + border-radius 5px + // border 1px solid white + .examples + display none + &::before + content "Examples" + font-weight bold + .example + position relative + + + &.collapsed + z-index 2 + width auto + min-width 50px + min-height 2em + max-width none + line-height 2 + + cursor pointer + text-align center + + * + display none + .shortname + display inline-block + min-width 50px + min-height auto + + + /* Category/Tag Colors {{{ */ + for i in 0...length($hilites) + $bg_color = $hilites[i] + $tag_color = $bg_color + contrast_direction($bg_color) * 50% + $fg_color = (lightness($bg_color) > 55%) ? $dark : $light + + &.category_{i} + color $fg_color + background-color $bg_color + // .shortname, .name, .close + // color $tag_color + label, h1, h2, h3, .shortname, .name, .close + color $fg_color + + .tag.category_{i} + // border 1px solid $tag_color + // color $tag_color + // background-color $bg_color + color $fg_color + background-color $bg_color + + /* }}} */ + + /* }}} End Chart Options */ + + + + diff --git a/www/graph/edit.jade b/www/graph/edit.jade index 4566fe5..d51b469 100644 --- a/www/graph/edit.jade +++ b/www/graph/edit.jade @@ -8,3 +8,5 @@ append styles mixin css('data.css') mixin css('isotope.css') +block main-scripts + script(src="/js/kraken/main-edit.js?"+version) diff --git a/www/graph/view.jade b/www/graph/view.jade index 699cfb3..daf3b67 100644 --- a/www/graph/view.jade +++ b/www/graph/view.jade @@ -1 +1,10 @@ -include edit \ No newline at end of file +extends ../layout + +block title + title Graph | GraphKit + +append styles + mixin css('graph-display.css') + +block main-scripts + script(src="/js/kraken/main-display.js?"+version)