-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
+ """
+ <span class="value"><span class="whole">#whole</span><span class="fraction">#fraction</span><span class="suffix">#suffix</span></span>
+ """
+
+ ### }}}
+ ### 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
+
+
+ # }}}
# }}}
+
-- 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}
- <textarea class="span3 desc" 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
- .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)
--- /dev/null
+@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 */
+
+
+
+