diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2014-04-29 22:38:08 +0600 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2014-04-30 11:35:17 +0600 |
commit | 9c811fbeea8de168d5927f610cc41af5bfffdc07 (patch) | |
tree | ea092327b345f54e5361330bae131a38e047da2a /sonar-server/src/main/coffee | |
parent | 74b529baad50b783069dfd23c68562ac1401ffbb (diff) | |
download | sonarqube-9c811fbeea8de168d5927f610cc41af5bfffdc07.tar.gz sonarqube-9c811fbeea8de168d5927f610cc41af5bfffdc07.zip |
Refactor widgets
Diffstat (limited to 'sonar-server/src/main/coffee')
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 |