aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-server/src/main/coffee
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2014-04-29 22:38:08 +0600
committerStas Vilchik <vilchiks@gmail.com>2014-04-30 11:35:17 +0600
commit9c811fbeea8de168d5927f610cc41af5bfffdc07 (patch)
treeea092327b345f54e5361330bae131a38e047da2a /sonar-server/src/main/coffee
parent74b529baad50b783069dfd23c68562ac1401ffbb (diff)
downloadsonarqube-9c811fbeea8de168d5927f610cc41af5bfffdc07.tar.gz
sonarqube-9c811fbeea8de168d5927f610cc41af5bfffdc07.zip
Refactor widgets
Diffstat (limited to 'sonar-server/src/main/coffee')
-rw-r--r--sonar-server/src/main/coffee/tests/widgets/BaseSpec.coffee47
-rw-r--r--sonar-server/src/main/coffee/widgets/base.coffee50
-rw-r--r--sonar-server/src/main/coffee/widgets/histogram.coffee142
-rw-r--r--sonar-server/src/main/coffee/widgets/word-cloud.coffee184
4 files changed, 309 insertions, 114 deletions
diff --git a/sonar-server/src/main/coffee/tests/widgets/BaseSpec.coffee b/sonar-server/src/main/coffee/tests/widgets/BaseSpec.coffee
new file mode 100644
index 00000000000..797acf75588
--- /dev/null
+++ b/sonar-server/src/main/coffee/tests/widgets/BaseSpec.coffee
@@ -0,0 +1,47 @@
+$ = jQuery
+
+describe 'base widget suite', ->
+
+ it 'exists', ->
+ expect(window.SonarWidgets).toBeDefined()
+ expect(window.SonarWidgets.BaseWidget).toBeDefined()
+
+
+ it 'adds fields', ->
+ widget = new window.SonarWidgets.BaseWidget()
+ widget.addField 'fieldName', 1
+
+ expect(typeof widget.fieldName).toBe 'function'
+ expect(widget.fieldName()).toBe 1
+
+ expect(widget.fieldName(2)).toBe widget
+ expect(widget.fieldName()).toBe 2
+
+
+ it 'adds metrics', ->
+ widget = new window.SonarWidgets.BaseWidget()
+ widget.addField 'metrics', 'metricA': { name: 'Metric A', someField: 2 }
+ widget.addField 'metricsPriority', ['metricA']
+ widget.addMetric 'myMetric', 0
+
+ expect(widget.myMetric).toBeDefined()
+ expect(widget.myMetric.key).toBe 'metricA'
+ expect(widget.myMetric.name).toBe 'Metric A'
+ expect(widget.myMetric.someField).toBe 2
+ expect(typeof widget.myMetric.value).toBe 'function'
+ expect(typeof widget.myMetric.formattedValue).toBe 'function'
+
+
+ it 'has default properties', ->
+ widget = new window.SonarWidgets.BaseWidget()
+
+ expect(widget.components).toBeDefined()
+ expect(widget.metrics).toBeDefined()
+ expect(widget.metricsPriority).toBeDefined()
+ expect(widget.options).toBeDefined()
+
+
+ it 'created "translate" string', ->
+ widget = new window.SonarWidgets.BaseWidget()
+
+ expect(widget.trans(1, 2)).toBe 'translate(1,2)' \ No newline at end of file
diff --git a/sonar-server/src/main/coffee/widgets/base.coffee b/sonar-server/src/main/coffee/widgets/base.coffee
new file mode 100644
index 00000000000..133c15e464d
--- /dev/null
+++ b/sonar-server/src/main/coffee/widgets/base.coffee
@@ -0,0 +1,50 @@
+window.SonarWidgets ?= {}
+
+class BaseWidget
+ lineHeight: 20
+
+
+ constructor: ->
+ @addField 'components', []
+ @addField 'metrics', []
+ @addField 'metricsPriority', []
+ @addField 'options', []
+ @
+
+
+ addField: (name, defaultValue) ->
+ privateName = "_#{name}"
+ @[privateName] = defaultValue
+ @[name] = (d) -> @param.call @, privateName, d
+ @
+
+
+ param: (name, value) ->
+ return @[name] unless value?
+ @[name] = value
+ @
+
+
+ addMetric: (property, index) ->
+ key = @metricsPriority()[index]
+ @[property] = _.extend @metrics()[key],
+ key: key
+ value: (d) -> d.measures[key]?.val
+ formattedValue: (d) -> d.measures[key]?.fval
+ @
+
+
+ trans: (left, top) ->
+ "translate(#{left},#{top})"
+
+
+ render: (container) ->
+ @update container
+ @
+
+
+ update: ->
+ @
+
+
+window.SonarWidgets.BaseWidget = BaseWidget \ No newline at end of file
diff --git a/sonar-server/src/main/coffee/widgets/histogram.coffee b/sonar-server/src/main/coffee/widgets/histogram.coffee
new file mode 100644
index 00000000000..2dacc0b73bc
--- /dev/null
+++ b/sonar-server/src/main/coffee/widgets/histogram.coffee
@@ -0,0 +1,142 @@
+class Histogram extends window.SonarWidgets.BaseWidget
+ barHeight: 16
+ barFill: '#1f77b4'
+
+ constructor: ->
+ @addField 'width', 0
+ @addField 'height', window.SonarWidgets.Histogram.defaults.height
+ @addField 'margin', window.SonarWidgets.Histogram.defaults.margin
+ @addField 'legendWidth', window.SonarWidgets.Histogram.defaults.legendWidth
+ @addField 'maxResultsReached', false
+ super
+
+
+ isDataValid: ->
+ @components().reduce ((p, c) => p && !! c.measures[@mainMetric.key]), true
+
+
+ truncate: (text, type) ->
+ maxLength = 40
+ switch type
+ when "FIL", "CLA"
+ n = text.length
+ if n > maxLength
+ shortText = text.substr(n - maxLength + 2, n - 1)
+ dotIndex = shortText.indexOf(".")
+ return "..." + shortText.substr(dotIndex + 1)
+ else
+ return text
+ else
+ (if text.length > maxLength then text.substr(0, maxLength - 3) + "..." else text)
+
+
+ render: (container) ->
+ box = d3.select container
+
+ # Configure the main metric
+ @addMetric 'mainMetric', 0
+
+ unless @isDataValid()
+ box.text @options().noMainMetric
+ return
+
+ @width box.property 'offsetWidth'
+
+ # Create skeleton
+ @svg = box.append('svg').classed 'sonar-d3', true
+ @gWrap = @svg.append 'g'
+ @gWrap.attr 'transform', @trans(@margin().left, @margin().top)
+ @plotWrap = @gWrap.append('g').classed 'plot', true
+
+ # Configure scales
+ @x = d3.scale.linear()
+ @y = d3.scale.ordinal()
+
+ @metricLabel = @gWrap.append('text').text @mainMetric.name
+ @metricLabel.attr('dy', '9px').style 'font-size', '12px'
+
+ if @maxResultsReached()
+ @maxResultsReachedLabel = @gWrap.append('text').classed 'max-results-reached-message', true
+ @maxResultsReachedLabel.text @options().maxItemsReachedMessage
+
+ super
+
+
+ update: (container) ->
+ box = d3.select(container)
+
+ @width box.property 'offsetWidth'
+ availableWidth = @width() - @margin().left - @margin().right - @legendWidth()
+ availableHeight = @barHeight * @components().length + @lineHeight
+ totalHeight = availableHeight + @margin().top + @margin().bottom
+ totalHeight += @lineHeight if @maxResultsReached()
+ @height totalHeight
+
+ @svg.attr('width', @width()).attr 'height', @height()
+ @plotWrap.attr 'transform', @trans 0, @lineHeight
+
+ # Configure scales
+ xDomain = d3.extent @components(), (d) => @mainMetric.value d
+ unless @options().relativeScale
+ if @mainMetric.type == 'PERCENT'
+ xDomain = [0, 100]
+ else
+ xDomain[0] = 0
+
+ @x.domain xDomain
+ .range [0, availableWidth]
+
+ @y.domain @components().map (d, i) -> i
+ .rangeRoundBands [0, availableHeight], 0
+
+ # Configure bars
+ @bars = @plotWrap.selectAll('.bar').data @components()
+ @barsEnter = @bars.enter().append 'g'
+ .classed 'bar', true
+ .attr 'transform', (d, i) => @trans 0, i * @barHeight
+ @barsEnter.append 'rect'
+ .style 'fill', @barFill
+ @barsEnter.append 'text'
+ .classed 'legend-text component', true
+ .style 'text-anchor', 'end'
+ .attr 'dy', '-0.35em'
+ .text (d) => @truncate d.longName, d.qualifier
+ .attr 'transform', => @trans @legendWidth() - 10, @barHeight
+ @barsEnter.append 'text'
+ .classed 'legend-text value', true
+ .attr 'dy', '-0.35em'
+ .text (d) => @mainMetric.formattedValue d
+ .attr 'transform', (d) => @trans @legendWidth() + @x(@mainMetric.value d) + 5, @barHeight
+ @bars.selectAll 'rect'
+ .transition()
+ .attr 'x', @legendWidth()
+ .attr 'y', 0
+ .attr 'width', (d) => Math.max 2, @x(@mainMetric.value d)
+ .attr 'height', @barHeight
+ @bars.selectAll '.component'
+ .transition()
+ .attr 'transform', => @trans @legendWidth() - 10, @barHeight
+ @bars.selectAll '.value'
+ .transition()
+ .attr 'transform', (d) => @trans @legendWidth() + @x(@mainMetric.value d) + 5, @barHeight
+ @bars.exit().remove()
+ @bars.on 'click', (d) =>
+ url = @options().baseUrl + encodeURIComponent d.key
+ if d.qualifier == 'CLA' || d.qualifier == 'FIL'
+ url += '?metric=' + encodeURIComponent @mainMetric.key
+ window.location = url
+
+ @metricLabel.attr 'transform', @trans @legendWidth(), 0
+
+ if @maxResultsReached()
+ @maxResultsReachedLabel.attr 'transform', (@trans @legendWidth(), @height() - @margin().bottom - 3)
+
+ super
+
+
+
+window.SonarWidgets.Histogram = Histogram
+window.SonarWidgets.Histogram.defaults =
+ height: 300
+ margin: { top: 4, right: 50, bottom: 4, left: 10 }
+ legendWidth: 220 \ No newline at end of file
diff --git a/sonar-server/src/main/coffee/widgets/word-cloud.coffee b/sonar-server/src/main/coffee/widgets/word-cloud.coffee
index 8b718ef50b7..1e09b55a6d5 100644
--- a/sonar-server/src/main/coffee/widgets/word-cloud.coffee
+++ b/sonar-server/src/main/coffee/widgets/word-cloud.coffee
@@ -1,114 +1,70 @@
-# Some helper functions
-
-# Gets or sets parameter
-param = (name, value) ->
- unless value?
- return this[name]
- else
- this[name] = value
- return @
-
-
-window.SonarWidgets ?= {}
-
-window.SonarWidgets.WordCloud = ->
- @_components = []
- @_metrics = []
- @_metricsPriority = []
- @_width = window.SonarWidgets.WordCloud.defaults.width
- @_height = window.SonarWidgets.WordCloud.defaults.height
- @_margin = window.SonarWidgets.WordCloud.defaults.margin
- @_legendWidth = window.SonarWidgets.WordCloud.defaults.legendWidth
- @_legendMargin = window.SonarWidgets.WordCloud.defaults.legendMargin
- @_detailsWidth = window.SonarWidgets.WordCloud.defaults.detailsWidth
- @_maxResultsReached = false
- @_options = {}
-
- @_lineHeight = 20
-
- # Export global variables
- @metrics = (_) -> param.call(this, '_metrics', _)
- @metricsPriority = (_) -> param.call(this, '_metricsPriority', _)
- @components = (_) -> param.call(this, '_components', _)
- @width = (_) -> param.call(this, '_width', _)
- @height = (_) -> param.call(this, '_height', _)
- @margin = (_) -> param.call(this, '_margin', _)
- @legendWidth = (_) -> param.call(this, '_legendWidth', _)
- @legendMargin = (_) -> param.call(this, '_legendMargin', _)
- @detailsWidth = (_) -> param.call(this, '_detailsWidth', _)
- @maxResultsReached = (_) -> param.call(this, '_maxResultsReached', _)
- @options = (_) -> param.call(this, '_options', _)
- @
-
-
-window.SonarWidgets.WordCloud.prototype.render = (container) ->
- @box = d3.select(container).append('div').classed 'sonar-d3', true
- @box.style 'text-align', 'center'
-
- # Configure metrics
- @colorMetric = @metricsPriority()[0]
- @getColorMetric = (d) => d.measures[@colorMetric]?.val
- @getFColorMetric = (d) => d.measures[@colorMetric]?.fval
- @sizeMetric = @metricsPriority()[1]
- @getSizeMetric = (d) => d.measures[@sizeMetric]?.val
- @getFSizeMetric = (d) => d.measures[@sizeMetric]?.fval
-
- # Configure scales
- @color = d3.scale.linear().domain([0, 100])
- if @metrics()[@colorMetric].direction == 1
- @color.range ['#d62728', '#1f77b4']
- else
- @color.range ['#1f77b4', '#d62728']
-
- sizeDomain = d3.extent @components(), (d) => @getSizeMetric d
- @size = d3.scale.linear().domain(sizeDomain).range([10, 24])
-
- @update container
- @
-
-
-window.SonarWidgets.WordCloud.prototype.update = ->
- # Configure words
- @words = @box.selectAll('a').data @components()
-
- wordsEnter = @words.enter().append('a')
- wordsEnter.classed 'cloud-word', true
- wordsEnter.text (d) -> d.name
- wordsEnter.attr 'href', (d) =>
- url = @options().baseUrl + encodeURIComponent(d.key)
- url += '?metric=' + encodeURIComponent(@colorMetric) if d.qualifier == 'CLA' || d.qualifier == 'FIL'
- url
- wordsEnter.attr 'title', (d) =>
- title = d.longName
- title += " | #{@metrics()[@colorMetric].name}: #{@getFColorMetric d}" if @getColorMetric(d)?
- title += " | #{@metrics()[@sizeMetric].name}: #{@getFSizeMetric d}" if @getSizeMetric(d)?
- title
-
- @words.style 'color', (d) =>
- if @getColorMetric(d)? then @color @getColorMetric d else '#999'
- @words.style 'font-size', (d) => "#{@size @getSizeMetric d}px"
-
- @words.sort (a, b) =>
- if a.name.toLowerCase() > b.name.toLowerCase() then 1 else -1
-
- # Show maxResultsReached message
- if @maxResultsReached()
- @maxResultsReachedLabel.remove() if @maxResultsReachedLabel?
- @maxResultsReachedLabel = @box.append('div').text @options().maxItemsReachedMessage
- @maxResultsReachedLabel.style 'color', '#777'
- @maxResultsReachedLabel.style 'font-size', '12px'
- @maxResultsReachedLabel.style 'text-align', 'center'
- @maxResultsReachedLabel.style 'margin-top', '10px'
-
- @words.exit().remove()
-
-
-
-
-
-window.SonarWidgets.WordCloud.defaults =
- width: 350
- height: 300
- margin: { top: 10, right: 10, bottom: 10, left: 10 }
- legendWidth: 160
- legendMargin: 30 \ No newline at end of file
+class WordCloud extends window.SonarWidgets.BaseWidget
+ colorLow: '#d62728'
+ colorHigh: '#1f77b4'
+ colorUnknown: '#777'
+ sizeLow: 10
+ sizeHigh: 24
+
+
+ constructor: ->
+ @addField 'width', []
+ @addField 'height', []
+ @addField 'maxResultsReached', false
+ super
+
+
+ renderWords: ->
+ words = @wordContainer.selectAll('.cloud-word').data @components()
+
+ wordsEnter = words.enter().append('a').classed 'cloud-word', true
+ wordsEnter.text (d) -> d.name
+ wordsEnter.attr 'href', (d) =>
+ url = @options().baseUrl + encodeURIComponent(d.key)
+ url += '?metric=' + encodeURIComponent(@colorMetric.key) if d.qualifier == 'CLA' || d.qualifier == 'FIL'
+ url
+ wordsEnter.attr 'title', (d) =>
+ title = d.longName
+ title += " | #{@colorMetric.name}: #{@colorMetric.formattedValue d}" if @colorMetric.value(d)?
+ title += " | #{@sizeMetric.name}: #{@sizeMetric.formattedValue d}" if @sizeMetric.value(d)?
+ title
+
+ words.style 'color', (d) =>
+ if @colorMetric.value(d)? then @color @colorMetric.value(d) else @colorUnknown
+ words.style 'font-size', (d) => "#{@size @sizeMetric.value d}px"
+
+ words.sort (a, b) =>
+ if a.name.toLowerCase() > b.name.toLowerCase() then 1 else -1
+
+
+ render: (container) ->
+ box = d3.select(container).append('div')
+ box.classed 'sonar-d3', true
+ box.classed 'cloud-widget', true
+ @wordContainer = box.append 'div'
+
+ # Configure metrics
+ @addMetric 'colorMetric', 0
+ @addMetric 'sizeMetric', 1
+
+ # Configure scales
+ @color = d3.scale.linear().domain([0, 100])
+ if @colorMetric.direction == 1
+ @color.range [@colorLow, @colorHigh]
+ else
+ @color.range [@colorHigh, @colorLow]
+
+ sizeDomain = d3.extent @components(), (d) => @sizeMetric.value d
+ @size = d3.scale.linear().domain(sizeDomain).range [@sizeLow, @sizeHigh]
+
+ # Show maxResultsReached message
+ if @maxResultsReached()
+ maxResultsReachedLabel = box.append('div').text @options().maxItemsReachedMessage
+ maxResultsReachedLabel.classed 'max-results-reached-message', true
+
+ @renderWords()
+
+ super
+
+
+
+window.SonarWidgets.WordCloud = WordCloud \ No newline at end of file