]> source.dussan.org Git - sonarqube.git/commitdiff
move widgets from coffee to js
authorStas Vilchik <vilchiks@gmail.com>
Fri, 29 May 2015 12:28:20 +0000 (14:28 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Fri, 29 May 2015 16:39:26 +0000 (18:39 +0200)
server/sonar-web/src/main/coffee/libs/widgets/base.coffee [deleted file]
server/sonar-web/src/main/coffee/libs/widgets/histogram.coffee [deleted file]
server/sonar-web/src/main/coffee/libs/widgets/tag-cloud.coffee [deleted file]
server/sonar-web/src/main/coffee/libs/widgets/treemap.coffee [deleted file]
server/sonar-web/src/main/coffee/libs/widgets/word-cloud.coffee [deleted file]
server/sonar-web/src/main/js/libs/widgets/base.js [new file with mode: 0644]
server/sonar-web/src/main/js/libs/widgets/histogram.js [new file with mode: 0644]
server/sonar-web/src/main/js/libs/widgets/tag-cloud.js [new file with mode: 0644]
server/sonar-web/src/main/js/libs/widgets/treemap.js [new file with mode: 0644]
server/sonar-web/src/main/js/libs/widgets/word-cloud.js [new file with mode: 0644]

diff --git a/server/sonar-web/src/main/coffee/libs/widgets/base.coffee b/server/sonar-web/src/main/coffee/libs/widgets/base.coffee
deleted file mode 100644 (file)
index d2f914b..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-#
-# SonarQube, open source software quality management tool.
-# Copyright (C) 2008-2014 SonarSource
-# mailto:contact AT sonarsource DOT com
-#
-# SonarQube is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 3 of the License, or (at your option) any later version.
-#
-# SonarQube is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-
-window.SonarWidgets ?= {}
-
-class BaseWidget
-  lineHeight: 20
-
-  colors4: ['#ee0000', '#f77700', '#80cc00', '#00aa00']
-  colors4r: ['#00aa00', '#80cc00', '#f77700', '#ee0000']
-  colors5: ['#ee0000', '#f77700', '#ffee00', '#80cc00', '#00aa00']
-  colors5r: ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000']
-  colorsLevel: ['#d4333f', '#ff9900', '#85bb43', '##b4b4b4']
-  colorUnknown: '#777'
-
-
-  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) ->
-        if d.measures[key]?
-          if d.measures[key].text? then d.measures[key].text else d.measures[key].val
-      formattedValue: (d) ->
-        if d.measures[key]?
-          if d.measures[key].text? then d.measures[key].text else d.measures[key].fval
-    @
-
-
-  trans: (left, top) ->
-    "translate(#{left},#{top})"
-
-
-  render: (container) ->
-    @update container
-    @
-
-
-  update: ->
-    @
-
-
-  tooltip: (d) ->
-    title = d.longName
-    title += "\n#{@colorMetric.name}: #{@colorMetric.formattedValue d}" if @colorMetric and @colorMetric.value(d)?
-    title += "\n#{@sizeMetric.name}: #{@sizeMetric.formattedValue d}" if @sizeMetric and @sizeMetric.value(d)?
-    title
-
-
-window.SonarWidgets.BaseWidget = BaseWidget
diff --git a/server/sonar-web/src/main/coffee/libs/widgets/histogram.coffee b/server/sonar-web/src/main/coffee/libs/widgets/histogram.coffee
deleted file mode 100644 (file)
index c62628a..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-#
-# SonarQube, open source software quality management tool.
-# Copyright (C) 2008-2014 SonarSource
-# mailto:contact AT sonarsource DOT com
-#
-# SonarQube is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 3 of the License, or (at your option) any later version.
-#
-# SonarQube is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-
-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) =>
-      window.location = @options().baseUrl + '?id=' + encodeURIComponent(d.key)
-
-    @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
diff --git a/server/sonar-web/src/main/coffee/libs/widgets/tag-cloud.coffee b/server/sonar-web/src/main/coffee/libs/widgets/tag-cloud.coffee
deleted file mode 100644 (file)
index f9202d8..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-#
-# SonarQube, open source software quality management tool.
-# Copyright (C) 2008-2014 SonarSource
-# mailto:contact AT sonarsource DOT com
-#
-# SonarQube is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 3 of the License, or (at your option) any later version.
-#
-# SonarQube is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-
-class TagCloud extends window.SonarWidgets.BaseWidget
-  sizeLow: 10
-  sizeHigh: 24
-
-
-  constructor: ->
-    @addField 'width', []
-    @addField 'height', []
-    @addField 'tags', []
-    @addField 'maxResultsReached', false
-    super
-
-
-  renderWords: ->
-    window.requestMessages().done =>
-      words = @wordContainer.selectAll('.cloud-word').data @tags()
-
-      wordsEnter = words.enter().append('a').classed 'cloud-word', true
-      wordsEnter.text (d) -> d.key
-      wordsEnter.attr 'href', (d) =>
-        url = @options().baseUrl + '|tags=' + d.key
-        if @options().createdAfter
-          url += '|createdAfter=' + @options().createdAfter
-        url
-      wordsEnter.attr 'title', (d) => @tooltip d
-
-      words.style 'font-size', (d) =>
-        "#{@size d.value}px"
-
-      words.sort (a, b) =>
-        if a.key.toLowerCase() > b.key.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'
-
-    sizeDomain = d3.extent @tags(), (d) => d.value
-    @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
-
-
-  tooltip: (d) ->
-    suffixKey = if d.value == 1 then 'issue' else 'issues'
-    suffix = t(suffixKey)
-    "#{d.value}\u00a0" + suffix
-
-
-  parseSource: (response) ->
-    @tags(response.tags)
-
-
-window.SonarWidgets.TagCloud = TagCloud
diff --git a/server/sonar-web/src/main/coffee/libs/widgets/treemap.coffee b/server/sonar-web/src/main/coffee/libs/widgets/treemap.coffee
deleted file mode 100644 (file)
index 9a78011..0000000
+++ /dev/null
@@ -1,254 +0,0 @@
-#
-# SonarQube, open source software quality management tool.
-# Copyright (C) 2008-2014 SonarSource
-# mailto:contact AT sonarsource DOT com
-#
-# SonarQube is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 3 of the License, or (at your option) any later version.
-#
-# SonarQube is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-
-class Treemap extends window.SonarWidgets.BaseWidget
-  sizeLow: 11
-  sizeHigh: 18
-
-
-  constructor: ->
-    @addField 'width', null
-    @addField 'height', null
-    @addField 'maxResultsReached', false
-    super
-
-
-  getNodes: ->
-    @treemap.nodes(children: @components()).filter (d) -> !d.children
-
-
-  renderTreemap: ->
-    nodes = @getNodes()
-    @cells = @box.selectAll('.treemap-cell').data nodes
-    @cells.exit().remove()
-
-    cellsEnter = @cells.enter().append 'div'
-    cellsEnter.classed 'treemap-cell', true
-    cellsEnter.append('div').classed 'treemap-inner', true
-    cellsEnter.append('a').classed 'treemap-link', true
-
-    @cells.attr 'title', (d) => @tooltip d
-    @cells.style 'background-color', (d) =>
-      if @colorMetric.value(d)? then @color @colorMetric.value(d) else @colorUnknown
-    @cells.classed 'treemap-cell-drilldown', (d) ->
-      d.qualifier? && d.qualifier != 'FIL' && d.qualifier != 'CLA'
-
-    prefix = @mostCommonPrefix _.pluck @components(), 'longName'
-    prefixLength = prefix.length
-    @cellsInner = @box.selectAll('.treemap-inner').data nodes
-    @cellsInner.html (d) ->
-      if prefixLength > 0
-        "#{prefix}<br>#{d.longName.substr prefixLength}"
-      else d.longName
-
-    @cellsLink = @box.selectAll('.treemap-link').data nodes
-    @cellsLink.html '<i class="icon-link"></i>'
-    @cellsLink.attr 'href', (d) =>
-      @options().baseUrl + '?id=' + encodeURIComponent(d.key)
-
-    @attachEvents cellsEnter
-
-    @maxResultsReachedLabel.style 'display', if @maxResultsReached() then 'block' else 'none'
-
-
-  updateTreemap: (components, maxResultsReached) ->
-    @components components
-    @maxResultsReached maxResultsReached
-
-    @renderTreemap()
-    @positionCells()
-
-
-  attachEvents: (cells) ->
-    cells.on 'click', (d) => @requestChildren d
-
-
-  positionCells: ->
-    @cells.style 'left', (d) -> "#{d.x}px"
-    @cells.style 'top', (d) -> "#{d.y}px"
-    @cells.style 'width', (d) -> "#{d.dx}px"
-    @cellsInner.style 'max-width', (d) -> "#{d.dx}px"
-    @cells.style 'height', (d) -> "#{d.dy}px"
-    @cells.style 'line-height', (d) -> "#{d.dy}px"
-    @cells.style 'font-size', (d) => "#{@size (d.dx / d.longName.length)}px"
-    @cellsLink.style 'font-size', (d) => "#{@sizeLink Math.min(d.dx, d.dy)}px"
-    @cells.classed 'treemap-cell-small', (d) -> (d.dx / d.longName.length) < 1 || d.dy < 40
-    @cells.classed 'treemap-cell-very-small', (d) -> d.dx < 24 || d.dy < 24
-
-
-  renderLegend: (box) ->
-    @legend = box.insert 'div', ':first-child'
-    @legend.classed 'legend', true
-    @legend.classed 'legend-html', true
-    @legend.append('span').classed('legend-text', true).html "Size: <span class='legend-text-main'>#{@sizeMetric.name}</span>"
-    @legend.append('span').classed('legend-text', true).html "Color: <span class='legend-text-main'>#{@colorMetric.name}</span>"
-
-
-  renderBreadcrumbs: (box) ->
-    @breadcrumbsBox = box.append('div').classed 'treemap-breadcrumbs', true
-    @breadcrumbs = []
-    d = name: '<i class="icon-home"></i>', components: @components(), maxResultsReached: @maxResultsReached()
-    @addToBreadcrumbs d
-
-
-  updateBreadcrumbs: ->
-    breadcrumbs = @breadcrumbsBox.selectAll('.treemap-breadcrumbs-item').data @breadcrumbs
-    breadcrumbs.exit().remove()
-    breadcrumbsEnter = breadcrumbs.enter().append('span').classed 'treemap-breadcrumbs-item', true
-    breadcrumbsEnter.attr 'title', (d) =>
-      if d.longName? then d.longName else @options().resource
-    breadcrumbsEnter.append('i').classed('icon-chevron-right', true).style 'display', (d, i) ->
-      if i > 0 then 'inline' else 'none'
-    breadcrumbsEnter.append('i').attr 'class', (d) ->
-      if d.qualifier? then "icon-qualifier-#{d.qualifier.toLowerCase()}" else ''
-    breadcrumbsEnterLinks = breadcrumbsEnter.append 'a'
-    breadcrumbsEnterLinks.html (d) -> d.name
-    breadcrumbsEnterLinks.on 'click', (d) =>
-      @updateTreemap d.components, d.maxResultsReached
-      @cutBreadcrumbs d
-    @breadcrumbsBox.style 'display', if @breadcrumbs.length < 2 then 'none' else 'block'
-
-
-  addToBreadcrumbs: (d) ->
-    @breadcrumbs.push d
-    @updateBreadcrumbs()
-
-
-  cutBreadcrumbs: (lastElement) ->
-    index = null
-    @breadcrumbs.forEach (d, i) ->
-      index = i if d.key == lastElement.key
-    if index?
-      @breadcrumbs = _.initial @breadcrumbs, @breadcrumbs.length - index - 1
-      @updateBreadcrumbs()
-
-
-  getColorScale: ->
-    return @getLevelColorScale() if @colorMetric.type == 'LEVEL'
-    return @getRatingColorScale() if @colorMetric.type == 'RATING'
-    @getPercentColorScale()
-
-
-  getPercentColorScale: ->
-    color = d3.scale.linear().domain([0, 25, 50, 75, 100])
-    color.range if @colorMetric.direction == 1 then @colors5 else @colors5r
-    color
-
-
-  getRatingColorScale: ->
-    domain = [1, 2, 3, 4, 5]
-    if @components().length > 0
-      colorMetricSample = @colorMetric.value _.first @components()
-      if typeof colorMetricSample == 'string'
-        domain = ['A', 'B', 'C', 'D', 'E']
-    color = d3.scale.ordinal().domain(domain).range @colors5r
-    color
-
-
-  getLevelColorScale: ->
-    color = d3.scale.ordinal().domain(['ERROR', 'WARN', 'OK', 'NONE']).range @colorsLevel
-    color
-
-
-  render: (container) ->
-    box = d3.select(container).append('div')
-    box.classed 'sonar-d3', true
-    @box = box.append('div').classed 'treemap-container', true
-
-    # Configure metrics
-    @addMetric 'colorMetric', 0
-    @addMetric 'sizeMetric', 1
-
-    # Configure scales
-    @color = @getColorScale()
-
-    @size = d3.scale.linear().domain([3, 15]).range([@sizeLow, @sizeHigh]).clamp true
-    @sizeLink = d3.scale.linear().domain([60, 100]).range([12, 14]).clamp true
-
-    @treemap = d3.layout.treemap()
-    @treemap.sort (a, b) -> a.value - b.value
-    @treemap.round true
-
-    @treemap.value (d) => @sizeMetric.value d
-
-    @maxResultsReachedLabel = box.append('div').text @options().maxItemsReachedMessage
-    @maxResultsReachedLabel.classed 'max-results-reached-message', true
-
-    @renderLegend box
-    @renderBreadcrumbs box
-    @renderTreemap()
-    super
-
-
-  update: ->
-    @width @box.property 'offsetWidth'
-    @height (@width() / 100.0 * @options().heightInPercents)
-    @box.style 'height', "#{@height()}px"
-    @treemap.size [@width(), @height()]
-    @cells.data @getNodes()
-    @positionCells()
-
-
-  formatComponents: (data) ->
-    components = _.filter data, (component) =>
-      hasSizeMetric = => _.findWhere component.msr, key: @sizeMetric.key
-      _.isArray(component.msr) && component.msr.length > 0 && hasSizeMetric()
-
-    if _.isArray(components) && components.length > 0
-      components.map (component) =>
-        measures = {}
-        component.msr.forEach (measure) ->
-          measures[measure.key] = val: measure.val, fval: measure.frmt_val
-
-        key: if component.copy? then component.copy else component.key
-        name: component.name
-        longName: component.lname
-        qualifier: component.qualifier
-        measures: measures
-
-
-  requestChildren: (d) ->
-    metrics = @metricsPriority().join ','
-    RESOURCES_URL = "#{baseUrl}/api/resources/index"
-    jQuery.get(RESOURCES_URL, resource: d.key, depth: 1, metrics: metrics).done (r) =>
-      components = @formatComponents r
-      if components?
-        components = _.sortBy components, (d) => -@sizeMetric.value d
-        components = _.initial components, components.length - @options().maxItems - 1
-        @updateTreemap components, components.length > @options().maxItems
-        @addToBreadcrumbs _.extend d, components: components, maxResultsReached: @maxResultsReached()
-
-
-  mostCommonPrefix: (strings) ->
-    sortedStrings = strings.slice(0).sort()
-    firstString = sortedStrings[0]
-    firstStringLength = firstString.length
-    lastString = sortedStrings[sortedStrings.length - 1]
-    i = 0
-    while i < firstStringLength && firstString.charAt(i) == lastString.charAt(i)
-      i++
-    prefix = firstString.substr 0, i
-    lastPrefixPart = _.last prefix.split /[\s\\\/]/
-    prefix.substr 0, prefix.length - lastPrefixPart.length
-
-
-
-
-window.SonarWidgets.Treemap = Treemap
diff --git a/server/sonar-web/src/main/coffee/libs/widgets/word-cloud.coffee b/server/sonar-web/src/main/coffee/libs/widgets/word-cloud.coffee
deleted file mode 100644 (file)
index 417fb6f..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-#
-# SonarQube, open source software quality management tool.
-# Copyright (C) 2008-2014 SonarSource
-# mailto:contact AT sonarsource DOT com
-#
-# SonarQube is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 3 of the License, or (at your option) any later version.
-#
-# SonarQube is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-
-class WordCloud extends window.SonarWidgets.BaseWidget
-  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) =>
-      @options().baseUrl + '?id=' + encodeURIComponent(d.key)
-    wordsEnter.attr 'title', (d) => @tooltip d
-
-    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, 33, 67, 100])
-    if @colorMetric.direction == 1
-      @color.range @colors4
-    else
-      @color.range @colors4r
-
-    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
diff --git a/server/sonar-web/src/main/js/libs/widgets/base.js b/server/sonar-web/src/main/js/libs/widgets/base.js
new file mode 100644 (file)
index 0000000..3a788a0
--- /dev/null
@@ -0,0 +1,93 @@
+(function () {
+
+  if (window.SonarWidgets == null) {
+    window.SonarWidgets = {};
+  }
+
+  function BaseWidget () {
+    this.addField('components', []);
+    this.addField('metrics', []);
+    this.addField('metricsPriority', []);
+    this.addField('options', []);
+  }
+
+  BaseWidget.prototype.lineHeight = 20;
+  BaseWidget.prototype.colors4 = ['#ee0000', '#f77700', '#80cc00', '#00aa00'];
+  BaseWidget.prototype.colors4r = ['#00aa00', '#80cc00', '#f77700', '#ee0000'];
+  BaseWidget.prototype.colors5 = ['#ee0000', '#f77700', '#ffee00', '#80cc00', '#00aa00'];
+  BaseWidget.prototype.colors5r = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
+  BaseWidget.prototype.colorsLevel = ['#d4333f', '#ff9900', '#85bb43', '##b4b4b4'];
+  BaseWidget.prototype.colorUnknown = '#777';
+
+
+  BaseWidget.prototype.addField = function (name, defaultValue) {
+    var privateName = '_' + name;
+    this[privateName] = defaultValue;
+    this[name] = function (d) {
+      return this.param.call(this, privateName, d);
+    };
+    return this;
+  };
+
+  BaseWidget.prototype.param = function (name, value) {
+    if (value == null) {
+      return this[name];
+    }
+    this[name] = value;
+    return this;
+  };
+
+  BaseWidget.prototype.addMetric = function (property, index) {
+    var key = this.metricsPriority()[index];
+    this[property] = _.extend(this.metrics()[key], {
+      key: key,
+      value: function (d) {
+        if (d.measures[key] != null) {
+          if (d.measures[key].text != null) {
+            return d.measures[key].text;
+          } else {
+            return d.measures[key].val;
+          }
+        }
+      },
+      formattedValue: function (d) {
+        if (d.measures[key] != null) {
+          if (d.measures[key].text != null) {
+            return d.measures[key].text;
+          } else {
+            return d.measures[key].fval;
+          }
+        }
+      }
+    });
+    return this;
+  };
+
+  BaseWidget.prototype.trans = function (left, top) {
+    return 'translate(' + left + ',' + top + ')';
+  };
+
+  BaseWidget.prototype.render = function (container) {
+    this.update(container);
+    return this;
+  };
+
+  BaseWidget.prototype.update = function () {
+    return this;
+  };
+
+  BaseWidget.prototype.tooltip = function (d) {
+    /* jshint nonbsp: false */
+    var title = d.longName;
+    if (this.colorMetric && (this.colorMetric.value(d) != null)) {
+      title += '\n' + this.colorMetric.name + ': ' + (this.colorMetric.formattedValue(d));
+    }
+    if (this.sizeMetric && (this.sizeMetric.value(d) != null)) {
+      title += '\n' + this.sizeMetric.name + ': ' + (this.sizeMetric.formattedValue(d));
+    }
+    return title;
+  };
+
+  window.SonarWidgets.BaseWidget = BaseWidget;
+
+})();
diff --git a/server/sonar-web/src/main/js/libs/widgets/histogram.js b/server/sonar-web/src/main/js/libs/widgets/histogram.js
new file mode 100644 (file)
index 0000000..df902e8
--- /dev/null
@@ -0,0 +1,164 @@
+(function () {
+
+  function Histogram () {
+    window.SonarWidgets.BaseWidget.apply(this, arguments);
+    this.addField('width', 0);
+    this.addField('height', window.SonarWidgets.Histogram.defaults.height);
+    this.addField('margin', window.SonarWidgets.Histogram.defaults.margin);
+    this.addField('legendWidth', window.SonarWidgets.Histogram.defaults.legendWidth);
+    this.addField('maxResultsReached', false);
+  }
+
+  Histogram.prototype = new window.SonarWidgets.BaseWidget();
+
+  Histogram.prototype.barHeight = 16;
+
+  Histogram.prototype.barFill = '#1f77b4';
+
+  Histogram.prototype.isDataValid = function () {
+    var that = this;
+    return this.components().reduce(function (p, c) {
+      return p && !!c.measures[that.mainMetric.key];
+    }, true);
+  };
+
+  Histogram.prototype.truncate = function (text, type) {
+    var maxLength = 40;
+    switch (type) {
+      case 'FIL':
+      case 'CLA':
+        var n = text.length;
+        if (n > maxLength) {
+          var shortText = text.substr(n - maxLength + 2, n - 1),
+              dotIndex = shortText.indexOf('.');
+          return '...' + shortText.substr(dotIndex + 1);
+        } else {
+          return text;
+        }
+        break;
+      default:
+        if (text.length > maxLength) {
+          return text.substr(0, maxLength - 3) + '...';
+        } else {
+          return text;
+        }
+    }
+  };
+
+  Histogram.prototype.render = function (container) {
+    var box = d3.select(container);
+    this.addMetric('mainMetric', 0);
+    if (!this.isDataValid()) {
+      box.text(this.options().noMainMetric);
+      return;
+    }
+    this.width(box.property('offsetWidth'));
+    this.svg = box.append('svg').classed('sonar-d3', true);
+    this.gWrap = this.svg.append('g');
+    this.gWrap.attr('transform', this.trans(this.margin().left, this.margin().top));
+    this.plotWrap = this.gWrap.append('g').classed('plot', true);
+    this.x = d3.scale.linear();
+    this.y = d3.scale.ordinal();
+    this.metricLabel = this.gWrap.append('text').text(this.mainMetric.name);
+    this.metricLabel.attr('dy', '9px').style('font-size', '12px');
+    if (this.maxResultsReached()) {
+      this.maxResultsReachedLabel = this.gWrap.append('text').classed('max-results-reached-message', true);
+      this.maxResultsReachedLabel.text(this.options().maxItemsReachedMessage);
+    }
+    return window.SonarWidgets.BaseWidget.prototype.render.apply(this, arguments);
+  };
+
+  Histogram.prototype.update = function (container) {
+    var that = this;
+    var box = d3.select(container);
+    this.width(box.property('offsetWidth'));
+    var availableWidth = this.width() - this.margin().left - this.margin().right - this.legendWidth(),
+        availableHeight = this.barHeight * this.components().length + this.lineHeight,
+        totalHeight = availableHeight + this.margin().top + this.margin().bottom;
+    if (this.maxResultsReached()) {
+      totalHeight += this.lineHeight;
+    }
+    this.height(totalHeight);
+    this.svg.attr('width', this.width()).attr('height', this.height());
+    this.plotWrap.attr('transform', this.trans(0, this.lineHeight));
+    var xDomain = d3.extent(this.components(), function (d) {
+      return that.mainMetric.value(d);
+    });
+    if (!this.options().relativeScale) {
+      if (this.mainMetric.type === 'PERCENT') {
+        xDomain = [0, 100];
+      } else {
+        xDomain[0] = 0;
+      }
+    }
+    this.x.domain(xDomain).range([0, availableWidth]);
+    this.y.domain(this.components().map(function (d, i) {
+      return i;
+    })).rangeRoundBands([0, availableHeight], 0);
+    this.bars = this.plotWrap.selectAll('.bar').data(this.components());
+    this.barsEnter = this.bars.enter().append('g').classed('bar', true).attr('transform', function (d, i) {
+      return that.trans(0, i * that.barHeight);
+    });
+    this.barsEnter.append('rect').style('fill', this.barFill);
+    this.barsEnter.append('text')
+        .classed('legend-text component', true)
+        .style('text-anchor', 'end')
+        .attr('dy', '-0.35em')
+        .text(function (d) {
+          return that.truncate(d.longName, d.qualifier);
+        })
+        .attr('transform', function () {
+          return that.trans(that.legendWidth() - 10, that.barHeight);
+        });
+    this.barsEnter.append('text')
+        .classed('legend-text value', true)
+        .attr('dy', '-0.35em')
+        .text(function (d) {
+          return that.mainMetric.formattedValue(d);
+        })
+        .attr('transform', function (d) {
+          return that.trans(that.legendWidth() + that.x(that.mainMetric.value(d)) + 5, that.barHeight);
+        });
+    this.bars.selectAll('rect')
+        .transition()
+        .attr('x', this.legendWidth())
+        .attr('y', 0)
+        .attr('width', function (d) {
+          return Math.max(2, that.x(that.mainMetric.value(d)));
+        })
+        .attr('height', this.barHeight);
+    this.bars.selectAll('.component')
+        .transition()
+        .attr('transform', function () {
+          return that.trans(that.legendWidth() - 10, that.barHeight);
+        });
+    this.bars.selectAll('.value')
+        .transition()
+        .attr('transform', function (d) {
+          return that.trans(that.legendWidth() + that.x(that.mainMetric.value(d)) + 5, that.barHeight);
+        });
+    this.bars.exit().remove();
+    this.bars.on('click', function (d) {
+      window.location = that.options().baseUrl + '?id=' + encodeURIComponent(d.key);
+    });
+    this.metricLabel.attr('transform', this.trans(this.legendWidth(), 0));
+    if (this.maxResultsReached()) {
+      this.maxResultsReachedLabel.attr('transform',
+          this.trans(this.legendWidth(), this.height() - this.margin().bottom - 3));
+    }
+    return window.SonarWidgets.BaseWidget.prototype.update.apply(this, arguments);
+  };
+
+  window.SonarWidgets.Histogram = Histogram;
+  window.SonarWidgets.Histogram.defaults = {
+    height: 300,
+    margin: {
+      top: 4,
+      right: 50,
+      bottom: 4,
+      left: 10
+    },
+    legendWidth: 220
+  };
+
+})();
diff --git a/server/sonar-web/src/main/js/libs/widgets/tag-cloud.js b/server/sonar-web/src/main/js/libs/widgets/tag-cloud.js
new file mode 100644 (file)
index 0000000..d09f38b
--- /dev/null
@@ -0,0 +1,77 @@
+(function () {
+
+  function TagCloud () {
+    window.SonarWidgets.BaseWidget.apply(this, arguments);
+    this.addField('width', []);
+    this.addField('height', []);
+    this.addField('tags', []);
+    this.addField('maxResultsReached', false);
+  }
+
+  TagCloud.prototype = new window.SonarWidgets.BaseWidget();
+
+  TagCloud.prototype.sizeHigh = 24;
+
+  TagCloud.prototype.sizeLow = 10;
+
+  TagCloud.prototype.renderWords = function () {
+    var that = this;
+    return window.requestMessages().done(function () {
+      var words = that.wordContainer.selectAll('.cloud-word').data(that.tags()),
+          wordsEnter = words.enter().append('a').classed('cloud-word', true);
+      wordsEnter.text(function (d) {
+        return d.key;
+      });
+      wordsEnter.attr('href', function (d) {
+        var url = that.options().baseUrl + '|tags=' + d.key;
+        if (that.options().createdAfter) {
+          url += '|createdAfter=' + that.options().createdAfter;
+        }
+        return url;
+      });
+      wordsEnter.attr('title', function (d) {
+        return that.tooltip(d);
+      });
+      words.style('font-size', function (d) {
+        return that.size(d.value) + 'px';
+      });
+      return words.sort(function (a, b) {
+        if (a.key.toLowerCase() > b.key.toLowerCase()) {
+          return 1;
+        } else {
+          return -1;
+        }
+      });
+    });
+  };
+
+  TagCloud.prototype.render = function (container) {
+    var box = d3.select(container).append('div');
+    box.classed('sonar-d3', true);
+    box.classed('cloud-widget', true);
+    this.wordContainer = box.append('div');
+    var sizeDomain = d3.extent(this.tags(), function (d) {
+      return d.value;
+    });
+    this.size = d3.scale.linear().domain(sizeDomain).range([this.sizeLow, this.sizeHigh]);
+    if (this.maxResultsReached()) {
+      var maxResultsReachedLabel = box.append('div').text(this.options().maxItemsReachedMessage);
+      maxResultsReachedLabel.classed('max-results-reached-message', true);
+    }
+    this.renderWords();
+    return window.SonarWidgets.BaseWidget.prototype.render.apply(this, arguments);
+  };
+
+  TagCloud.prototype.tooltip = function (d) {
+    var suffixKey = d.value === 1 ? 'issue' : 'issues',
+        suffix = t(suffixKey);
+    return (d.value + '\u00a0') + suffix;
+  };
+
+  TagCloud.prototype.parseSource = function (response) {
+    return this.tags(response.tags);
+  };
+
+  window.SonarWidgets.TagCloud = TagCloud;
+
+})();
diff --git a/server/sonar-web/src/main/js/libs/widgets/treemap.js b/server/sonar-web/src/main/js/libs/widgets/treemap.js
new file mode 100644 (file)
index 0000000..f9cc199
--- /dev/null
@@ -0,0 +1,329 @@
+(function () {
+
+  function Treemap () {
+    this.addField('width', null);
+    this.addField('height', null);
+    this.addField('maxResultsReached', false);
+    window.SonarWidgets.BaseWidget.apply(this, arguments);
+  }
+
+  Treemap.prototype = new window.SonarWidgets.BaseWidget();
+
+  Treemap.prototype.sizeLow = 11;
+
+  Treemap.prototype.sizeHigh = 18;
+
+  Treemap.prototype.getNodes = function () {
+    return this.treemap
+        .nodes({ children: this.components() })
+        .filter(function (d) {
+          return !d.children;
+        });
+  };
+
+  Treemap.prototype.renderTreemap = function () {
+    var that = this;
+    var nodes = this.getNodes();
+    this.cells = this.box.selectAll('.treemap-cell').data(nodes);
+    this.cells.exit().remove();
+    var cellsEnter = this.cells.enter().append('div');
+    cellsEnter.classed('treemap-cell', true);
+    cellsEnter.append('div').classed('treemap-inner', true);
+    cellsEnter.append('a').classed('treemap-link', true);
+    this.cells.attr('title', function (d) {
+      return that.tooltip(d);
+    });
+    this.cells.style('background-color', function (d) {
+      if (that.colorMetric.value(d) != null) {
+        return that.color(that.colorMetric.value(d));
+      } else {
+        return that.colorUnknown;
+      }
+    });
+    this.cells.classed('treemap-cell-drilldown', function (d) {
+      return (d.qualifier != null) && d.qualifier !== 'FIL' && d.qualifier !== 'CLA';
+    });
+    var prefix = this.mostCommonPrefix(_.pluck(this.components(), 'longName')),
+        prefixLength = prefix.length;
+    this.cellsInner = this.box.selectAll('.treemap-inner').data(nodes);
+    this.cellsInner.html(function (d) {
+      if (prefixLength > 0) {
+        return prefix + '<br>' + (d.longName.substr(prefixLength));
+      } else {
+        return d.longName;
+      }
+    });
+    this.cellsLink = this.box.selectAll('.treemap-link').data(nodes);
+    this.cellsLink.html('<i class="icon-link"></i>');
+    this.cellsLink.attr('href', function (d) {
+      return that.options().baseUrl + '?id=' + encodeURIComponent(d.key);
+    });
+    this.attachEvents(cellsEnter);
+    return this.maxResultsReachedLabel.style('display', this.maxResultsReached() ? 'block' : 'none');
+  };
+
+  Treemap.prototype.updateTreemap = function (components, maxResultsReached) {
+    this.components(components);
+    this.maxResultsReached(maxResultsReached);
+    this.renderTreemap();
+    return this.positionCells();
+  };
+
+  Treemap.prototype.attachEvents = function (cells) {
+    var that = this;
+    return cells.on('click', function (d) {
+      return that.requestChildren(d);
+    });
+  };
+
+  Treemap.prototype.positionCells = function () {
+    var that = this;
+    this.cells.style('left', function (d) {
+      return d.x + 'px';
+    });
+    this.cells.style('top', function (d) {
+      return d.y + 'px';
+    });
+    this.cells.style('width', function (d) {
+      return d.dx + 'px';
+    });
+    this.cellsInner.style('max-width', function (d) {
+      return d.dx + 'px';
+    });
+    this.cells.style('height', function (d) {
+      return d.dy + 'px';
+    });
+    this.cells.style('line-height', function (d) {
+      return d.dy + 'px';
+    });
+    this.cells.style('font-size', function (d) {
+      return (that.size(d.dx / d.longName.length)) + 'px';
+    });
+    this.cellsLink.style('font-size', function (d) {
+      return (that.sizeLink(Math.min(d.dx, d.dy))) + 'px';
+    });
+    this.cells.classed('treemap-cell-small', function (d) {
+      return (d.dx / d.longName.length) < 1 || d.dy < 40;
+    });
+    return this.cells.classed('treemap-cell-very-small', function (d) {
+      return d.dx < 24 || d.dy < 24;
+    });
+  };
+
+  Treemap.prototype.renderLegend = function (box) {
+    this.legend = box.insert('div', ':first-child');
+    this.legend.classed('legend', true);
+    this.legend.classed('legend-html', true);
+    this.legend.append('span')
+        .classed('legend-text', true)
+        .html('Size: <span class="legend-text-main">' + this.sizeMetric.name + '</span>');
+    return this.legend.append('span')
+        .classed('legend-text', true)
+        .html('Color: <span class="legend-text-main">' + this.colorMetric.name + '</span>');
+  };
+
+  Treemap.prototype.renderBreadcrumbs = function (box) {
+    this.breadcrumbsBox = box.append('div').classed('treemap-breadcrumbs', true);
+    this.breadcrumbs = [];
+    var d = {
+      name: '<i class="icon-home"></i>',
+      components: this.components(),
+      maxResultsReached: this.maxResultsReached()
+    };
+    return this.addToBreadcrumbs(d);
+  };
+
+  Treemap.prototype.updateBreadcrumbs = function () {
+    var that = this;
+    var breadcrumbs = this.breadcrumbsBox.selectAll('.treemap-breadcrumbs-item').data(this.breadcrumbs);
+    breadcrumbs.exit().remove();
+    var breadcrumbsEnter = breadcrumbs.enter().append('span').classed('treemap-breadcrumbs-item', true);
+    breadcrumbsEnter.attr('title', function (d) {
+      if (d.longName != null) {
+        return d.longName;
+      } else {
+        return that.options().resource;
+      }
+    });
+    breadcrumbsEnter.append('i')
+        .classed('icon-chevron-right', true)
+        .style('display', function (d, i) {
+          if (i > 0) {
+            return 'inline';
+          } else {
+            return 'none';
+          }
+        });
+    breadcrumbsEnter.append('i').attr('class', function (d) {
+      if (d.qualifier != null) {
+        return 'icon-qualifier-' + (d.qualifier.toLowerCase());
+      } else {
+        return '';
+      }
+    });
+    var breadcrumbsEnterLinks = breadcrumbsEnter.append('a');
+    breadcrumbsEnterLinks.html(function (d) {
+      return d.name;
+    });
+    breadcrumbsEnterLinks.on('click', function (d) {
+      that.updateTreemap(d.components, d.maxResultsReached);
+      return that.cutBreadcrumbs(d);
+    });
+    this.breadcrumbsBox.style('display', this.breadcrumbs.length < 2 ? 'none' : 'block');
+  };
+
+  Treemap.prototype.addToBreadcrumbs = function (d) {
+    this.breadcrumbs.push(d);
+    this.updateBreadcrumbs();
+  };
+
+  Treemap.prototype.cutBreadcrumbs = function (lastElement) {
+    var index = null;
+    this.breadcrumbs.forEach(function (d, i) {
+      if (d.key === lastElement.key) {
+        index = i;
+      }
+    });
+    if (index != null) {
+      this.breadcrumbs = _.initial(this.breadcrumbs, this.breadcrumbs.length - index - 1);
+      this.updateBreadcrumbs();
+    }
+  };
+
+  Treemap.prototype.getColorScale = function () {
+    if (this.colorMetric.type === 'LEVEL') {
+      return this.getLevelColorScale();
+    }
+    if (this.colorMetric.type === 'RATING') {
+      return this.getRatingColorScale();
+    }
+    return this.getPercentColorScale();
+  };
+
+  Treemap.prototype.getPercentColorScale = function () {
+    var color = d3.scale.linear().domain([0, 25, 50, 75, 100]);
+    color.range(this.colorMetric.direction === 1 ? this.colors5 : this.colors5r);
+    return color;
+  };
+
+  Treemap.prototype.getRatingColorScale = function () {
+    var domain = [1, 2, 3, 4, 5];
+    if (this.components().length > 0) {
+      var colorMetricSample = this.colorMetric.value(_.first(this.components()));
+      if (typeof colorMetricSample === 'string') {
+        domain = ['A', 'B', 'C', 'D', 'E'];
+      }
+    }
+    return d3.scale.ordinal().domain(domain).range(this.colors5r);
+  };
+
+  Treemap.prototype.getLevelColorScale = function () {
+    return d3.scale.ordinal().domain(['ERROR', 'WARN', 'OK', 'NONE']).range(this.colorsLevel);
+  };
+
+  Treemap.prototype.render = function (container) {
+    var that = this;
+    var box = d3.select(container).append('div');
+    box.classed('sonar-d3', true);
+    this.box = box.append('div').classed('treemap-container', true);
+    this.addMetric('colorMetric', 0);
+    this.addMetric('sizeMetric', 1);
+    this.color = this.getColorScale();
+    this.size = d3.scale.linear().domain([3, 15]).range([this.sizeLow, this.sizeHigh]).clamp(true);
+    this.sizeLink = d3.scale.linear().domain([60, 100]).range([12, 14]).clamp(true);
+    this.treemap = d3.layout.treemap();
+    this.treemap.sort(function (a, b) {
+      return a.value - b.value;
+    });
+    this.treemap.round(true);
+    this.treemap.value(function (d) {
+      return that.sizeMetric.value(d);
+    });
+    this.maxResultsReachedLabel = box.append('div').text(this.options().maxItemsReachedMessage);
+    this.maxResultsReachedLabel.classed('max-results-reached-message', true);
+    this.renderLegend(box);
+    this.renderBreadcrumbs(box);
+    this.renderTreemap();
+    return window.SonarWidgets.BaseWidget.prototype.render.apply(this, arguments);
+  };
+
+  Treemap.prototype.update = function () {
+    this.width(this.box.property('offsetWidth'));
+    this.height(this.width() / 100.0 * this.options().heightInPercents);
+    this.box.style('height', (this.height()) + 'px');
+    this.treemap.size([this.width(), this.height()]);
+    this.cells.data(this.getNodes());
+    return this.positionCells();
+  };
+
+  Treemap.prototype.formatComponents = function (data) {
+    var that = this;
+    var components = _.filter(data, function (component) {
+      var hasSizeMetric = function () {
+        return _.findWhere(component.msr, {
+          key: that.sizeMetric.key
+        });
+      };
+      return _.isArray(component.msr) && component.msr.length > 0 && hasSizeMetric();
+    });
+    if (_.isArray(components) && components.length > 0) {
+      return components.map(function (component) {
+        var measures = {};
+        component.msr.forEach(function (measure) {
+          measures[measure.key] = {
+            val: measure.val,
+            fval: measure.frmt_val
+          };
+        });
+        return {
+          key: component.copy != null ? component.copy : component.key,
+          name: component.name,
+          longName: component.lname,
+          qualifier: component.qualifier,
+          measures: measures
+        };
+      });
+    }
+  };
+
+  Treemap.prototype.requestChildren = function (d) {
+    var that = this;
+    var metrics = this.metricsPriority().join(','),
+        RESOURCES_URL = baseUrl + '/api/resources/index';
+    return jQuery.get(RESOURCES_URL, {
+      resource: d.key,
+      depth: 1,
+      metrics: metrics
+    }).done(function (r) {
+      var components = that.formatComponents(r);
+      if (components != null) {
+        components = _.sortBy(components, function (d) {
+          return -that.sizeMetric.value(d);
+        });
+        components = _.initial(components, components.length - that.options().maxItems - 1);
+        that.updateTreemap(components, components.length > that.options().maxItems);
+        return that.addToBreadcrumbs(_.extend(d, {
+          components: components,
+          maxResultsReached: that.maxResultsReached()
+        }));
+      }
+    });
+  };
+
+  Treemap.prototype.mostCommonPrefix = function (strings) {
+    var sortedStrings = strings.slice(0).sort(),
+        firstString = sortedStrings[0],
+        firstStringLength = firstString.length,
+        lastString = sortedStrings[sortedStrings.length - 1],
+        i = 0;
+    while (i < firstStringLength && firstString.charAt(i) === lastString.charAt(i)) {
+      i++;
+    }
+    var prefix = firstString.substr(0, i),
+        lastPrefixPart = _.last(prefix.split(/[\s\\\/]/));
+    return prefix.substr(0, prefix.length - lastPrefixPart.length);
+  };
+
+  window.SonarWidgets.Treemap = Treemap;
+
+})();
diff --git a/server/sonar-web/src/main/js/libs/widgets/word-cloud.js b/server/sonar-web/src/main/js/libs/widgets/word-cloud.js
new file mode 100644 (file)
index 0000000..c12e023
--- /dev/null
@@ -0,0 +1,76 @@
+(function () {
+
+  function WordCloud () {
+    this.addField('width', []);
+    this.addField('height', []);
+    this.addField('maxResultsReached', false);
+    window.SonarWidgets.BaseWidget.apply(this, arguments);
+  }
+
+  WordCloud.prototype = new window.SonarWidgets.BaseWidget();
+
+  WordCloud.prototype.sizeHigh = 24;
+
+  WordCloud.prototype.sizeLow = 10;
+
+  WordCloud.prototype.renderWords = function () {
+    var that = this;
+    var words = this.wordContainer.selectAll('.cloud-word').data(this.components()),
+        wordsEnter = words.enter().append('a').classed('cloud-word', true);
+    wordsEnter.text(function (d) {
+      return d.name;
+    });
+    wordsEnter.attr('href', function (d) {
+      return that.options().baseUrl + '?id=' + encodeURIComponent(d.key);
+    });
+    wordsEnter.attr('title', function (d) {
+      return that.tooltip(d);
+    });
+    words.style('color', function (d) {
+      if (that.colorMetric.value(d) != null) {
+        return that.color(that.colorMetric.value(d));
+      } else {
+        return that.colorUnknown;
+      }
+    });
+    words.style('font-size', function (d) {
+      return (that.size(that.sizeMetric.value(d))) + 'px';
+    });
+    return words.sort(function (a, b) {
+      if (a.name.toLowerCase() > b.name.toLowerCase()) {
+        return 1;
+      } else {
+        return -1;
+      }
+    });
+  };
+
+  WordCloud.prototype.render = function (container) {
+    var that = this;
+    var box = d3.select(container).append('div');
+    box.classed('sonar-d3', true);
+    box.classed('cloud-widget', true);
+    this.wordContainer = box.append('div');
+    this.addMetric('colorMetric', 0);
+    this.addMetric('sizeMetric', 1);
+    this.color = d3.scale.linear().domain([0, 33, 67, 100]);
+    if (this.colorMetric.direction === 1) {
+      this.color.range(this.colors4);
+    } else {
+      this.color.range(this.colors4r);
+    }
+    var sizeDomain = d3.extent(this.components(), function (d) {
+      return that.sizeMetric.value(d);
+    });
+    this.size = d3.scale.linear().domain(sizeDomain).range([this.sizeLow, this.sizeHigh]);
+    if (this.maxResultsReached()) {
+      var maxResultsReachedLabel = box.append('div').text(this.options().maxItemsReachedMessage);
+      maxResultsReachedLabel.classed('max-results-reached-message', true);
+    }
+    this.renderWords();
+    return window.SonarWidgets.BaseWidget.prototype.render.apply(this, arguments);
+  };
+
+  window.SonarWidgets.WordCloud = WordCloud;
+
+})();