extensions.add(UnplannedReviewsWidget.class);
extensions.add(ActionPlansWidget.class);
extensions.add(ReviewsMetricsWidget.class);
+ extensions.add(TreemapWidget.class);
// dashboards
extensions.add(DefaultDashboard.class);
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.widgets;
+
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.web.*;
+
+@WidgetCategory("Reporting")
+@WidgetProperties({
+ @WidgetProperty(key = "heightInPercents", type= WidgetPropertyType.INTEGER, defaultValue = "100", description = "Height, in percents of width"),
+ @WidgetProperty(key = "sizeMetric", type= WidgetPropertyType.METRIC, defaultValue = CoreMetrics.NCLOC_KEY, description = "Default metric for size"),
+ @WidgetProperty(key = "colorMetric", type= WidgetPropertyType.METRIC, defaultValue = CoreMetrics.VIOLATIONS_DENSITY_KEY, description = "Default metric for color")
+})
+public class TreemapWidget extends AbstractRubyTemplate implements RubyRailsWidget {
+ public String getId() {
+ return "tm"; // avoid conflict with CSS style "treemap"
+ }
+
+ public String getTitle() {
+ return "Treemap of components";
+ }
+
+ @Override
+ protected String getTemplatePath() {
+ return "/Users/sbrandhof/projects/github/sonar/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/treemap.html.erb";
+ }
+}
\ No newline at end of file
--- /dev/null
+<%= render :partial => 'treemap/treemap_container', :locals => {
+ :treemap_id => widget.id,
+ :size_metric => widget_properties['sizeMetric'],
+ :color_metric => widget_properties['colorMetric'],
+ :heightInPercents => widget_properties['heightInPercents'],
+ :resource_id => @resource.id
+ } -%>
\ No newline at end of file
def load_resource
@resource=Project.by_key(params[:id])
not_found("Resource not found") unless @resource
+
+ @resource=Project.find(@resource.copy_resource_id) if @resource.copy_resource_id
+
access_denied unless has_role?(:user, @resource)
@snapshot = @resource.last_snapshot
not_found("Snapshot not found") unless @snapshot
--- /dev/null
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2012 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# Sonar 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.
+#
+# Sonar 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 Sonar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+#
+class TreemapController < ApplicationController
+ helper :metrics
+
+ SECTION=Navigation::SECTION_HOME
+
+ def index
+ html_id = params[:id]
+ bad_request('Missing required property: id') if html_id.blank?
+
+ width = params[:width]
+ bad_request('Missing required property: width') if width.blank?
+ bad_request('Bad width') if width.to_i<=0
+
+ height = params[:height]
+ bad_request('Missing required property: height') if height.blank?
+ bad_request('Bad height') if height.to_i<=0
+
+ size_metric=Metric.by_key(params[:size_metric]||'lines')
+ bad_request('Unknown metric: ' + params[:size_metric]) unless size_metric
+
+ color_metric=(params[:color_metric].present? ? Metric.by_key(params[:color_metric]) : nil)
+
+ resource = nil
+ if params[:resource]
+ resource = Project.by_key(params[:resource])
+ bad_request('Unknown resource: ' + params[:resource]) unless resource
+ access_denied unless has_role?(:user, resource)
+ end
+
+ treemap = Treemap2.new(html_id, size_metric, width.to_i, height.to_i, {
+ :color_metric => color_metric,
+ :root_snapshot => (resource ? resource.last_snapshot : nil),
+ :period_index => params[:period_index].to_i,
+ :browsable => true
+ })
+
+ render :update do |page|
+ page.replace_html "tm-#{html_id}", :partial => 'treemap', :object => treemap
+ page.replace_html "tm-gradient-#{html_id}", :partial => 'gradient', :locals => {:metric => color_metric}
+ end
+ end
+
+end
--- /dev/null
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2012 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# Sonar 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.
+#
+# Sonar 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 Sonar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+#
+class Treemap2
+ include ActionView::Helpers::UrlHelper
+
+ attr_accessor :size_metric, :color_metric, :width, :height, :root_snapshot, :period_index,
+ :id, :id_counter, :measures, :browsable
+
+ def initialize(id, size_metric, width, height, options={})
+ @id_counter = 0
+ @id = id
+ @size_metric = size_metric
+ @width = width
+ @height = height
+
+ @color_metric = options[:color_metric]
+ @root_snapshot = options[:root_snapshot]
+ @measures = options[:measures] # pre-computed measures, for example by filters
+ if options[:period_index] && options[:period_index]>0
+ @period_index = options[:period_index]
+ end
+ end
+
+ def generate_html
+ root = Treemap::Node.new(:id => -1, :label => '')
+ build_tree(root)
+
+ output = Sonar::HtmlOutput.new do |o|
+ o.width = @width
+ o.height = @height
+ o.full_html = false
+ o.details_at_depth = 1
+ end
+ output.to_html(root)
+ end
+
+ def empty?
+ @id_counter==0
+ end
+
+ protected
+
+ def measures
+ @measures ||=
+ begin
+ metric_ids=[@size_metric.id]
+ metric_ids << @color_metric.id if @color_metric && @color_metric.id!=@size_metric.id
+
+ sql_conditions = 'snapshots.islast=? AND project_measures.characteristic_id IS NULL and project_measures.rule_id IS NULL ' +
+ 'and project_measures.rule_priority IS NULL and project_measures.metric_id in (?)'
+ sql_values = [true, metric_ids]
+ if @root_snapshot
+ sql_conditions += " AND snapshots.parent_snapshot_id=?"
+ sql_values << @root_snapshot.id
+ else
+ sql_conditions<<" AND snapshots.scope='PRJ' and snapshots.qualifier='TRK'"
+ end
+
+ ProjectMeasure.find(:all, :include => {:snapshot => :project}, :conditions => [sql_conditions].concat(sql_values))
+ end
+ end
+
+ def build_tree(node)
+ color_measures_by_sid={}
+ if @color_metric
+ measures.each do |measure|
+ color_measures_by_sid[measure.snapshot_id]=measure if measure.metric_id==@color_metric.id
+ end
+ end
+
+ measures.each do |measure|
+ if measure.metric_id==@size_metric.id
+ color_measure = color_measures_by_sid[measure.snapshot_id]
+ resource = measure.snapshot.project
+ child = Treemap::Node.new(:id => "#{@id}-#{@id_counter += 1}",
+ :size => size_value(measure),
+ :label => resource.name(false),
+ :title => escape_javascript(resource.name(true)),
+ :tooltip => tooltip(resource, measure, color_measure),
+ :color => html_color(color_measure),
+ :rid => resource.id,
+ :browsable => resource.display_dashboard?)
+ node.add_child(child)
+ end
+ end
+ end
+
+ def tooltip(resource,size_measure, color_measure)
+ html=CGI::escapeHTML(resource.name(true))
+ html += " - #{CGI::escapeHTML(@size_metric.short_name)}: #{CGI::escapeHTML(size_measure.formatted_value)}"
+ if color_measure
+ html += " - #{CGI::escapeHTML(@color_metric.short_name)}: #{CGI::escapeHTML(color_measure.formatted_value)}"
+ end
+ html
+ end
+
+ def size_value(measure)
+ if @period_index
+ var=measure.variation(@period_index)
+ var ? var.to_f.abs : 0.0
+ elsif measure.value
+ measure.value.to_f.abs||0.0
+ else
+ 0.0
+ end
+ end
+
+ def html_color(measure)
+ MeasureColor.color(measure).html
+ end
+
+end
+
+class Sonar::HtmlOutput < Treemap::HtmlOutput
+
+ def draw_node(node)
+ return "" if node.bounds.nil?
+
+ html = ''
+ html += "<div id=\"node-#{node.id}\" style=\""
+ html += "overflow:hidden;position:absolute;"
+ html += "left:#{node.bounds.x1}px; top:#{node.bounds.y1}px;"
+ html += "width:#{node.bounds.width}px;height: #{node.bounds.height}px;"
+ html += "background-color:#FFF;"
+ html += "\" alt='#{node.tooltip}' title='#{node.tooltip}'>"
+ html += "<div rid='#{node.rid}' id=\"tm-node-#{node.id}\" style='margin: 1px;background-color: #{node.color}; height: #{node.bounds.height-4}px;
+border: 1px solid #{node.color};' "
+ if node.browsable
+ html += "b=1 "
+ end
+ if @details_at_depth==node.depth
+ html += "onmouseover=\"this.style.borderColor='#444';\" onmouseout=\"this.style.borderColor='#{node.color}';\""
+ end
+ html += ' >'
+ html += draw_node_body(node)
+
+ if (!node.children.nil? && node.children.size > 0)
+ node.children.each do |c|
+ html += draw_node(c)
+ end
+ end
+ html += "</div></div>"
+ end
+
+ def draw_label(node)
+ label= "<a href='#' onclick='return openResource(#{node.rid})'>"
+ label += node_label(node)
+ label += "</a>"
+ label
+ end
+
+ def draw_tooltips(node)
+ ''
+ end
+end
\ No newline at end of file
id = 'treemap_gradient_direction_negative'
end
%>
- <%= min -%><%= color_metric.suffix -%> <img id="<%= id -%>" src="<%= image_path image -%>" style="border: 1px solid #000; vertical-align:middle"> <%= max -%><%= color_metric.suffix -%>
+ <span class="note"><%= min -%><%= color_metric.suffix -%> <img id="<%= id -%>" src="<%= image_path image -%>" style="border: 1px solid #000; vertical-align:middle"> <%= max
+ -%><%= color_metric.suffix -%></span>
<%
end
%>
\ No newline at end of file
--- /dev/null
+<%
+if metric && metric.worst_value && metric.best_value
+ if metric.worst_value<metric.best_value
+ min=metric.worst_value
+ max=metric.best_value
+ image = 'treemap_gradient.png'
+ id = 'treemap_gradient_direction_positive'
+ else
+ min=metric.best_value
+ max=metric.worst_value
+ image = 'treemap_gradient_inverted.png'
+ id = 'treemap_gradient_direction_negative'
+ end
+%>
+ <%= min -%><%= metric.suffix -%> <img id="<%= id -%>" src="<%= image_path image -%>" style="border: 1px solid #000; vertical-align:middle"> <%= max
+ -%><%= metric.suffix -%>
+<%
+end
+%>
\ No newline at end of file
--- /dev/null
+<div style="width: <%= treemap.width -%>px; height:<%= treemap.height %>px;">
+ <%= treemap.generate_html() -%>
+</div>
+<script>
+ for (var i = 1; i <= <%= treemap.id_counter -%>; i++) {
+ addTmEvent(<%= treemap.id -%>, i);
+ }
+ $('tm-loading-<%= treemap.id -%>').hide();
+</script>
--- /dev/null
+<div id="tm-container-<%= treemap_id -%>">
+ <table width="100%" class="spacer-bottom">
+ <tr>
+ <td valign="top" class="thin nowrap">
+ <span class="comments"><%= message('size') -%></span>
+ <br/>
+ <%= select_tag "size", options_grouped_by_domain(Sonar::TreemapBuilder.size_metrics, (size_metric ? size_metric.key : nil), :include_empty => true),
+ :id => "tm-size-#{treemap_id}", :class => 'small spacer-right', :onchange => "refreshTm(#{treemap_id}, #{resource_id})" %>
+ </td>
+ <td valign="top" class="thin nowrap">
+ <span class="comments"><%= message('color') -%></span>
+ <span id="tm-gradient-<%= treemap_id -%>" class="note"></span>
+ <br/>
+ <%= select_tag 'color', options_grouped_by_domain(Sonar::TreemapBuilder.color_metrics, (color_metric ? color_metric.key : nil), :include_empty => true),
+ :id => "tm-color-#{treemap_id}", :class => 'small', :onchange => "refreshTm(#{treemap_id}, #{resource_id})" %>
+ <%= image_tag 'loading.gif', :id => "tm-loading-#{treemap_id}", :style => 'vertical-align: top;display: none' -%>
+ </td>
+ <td></td>
+ </tr>
+ </table>
+ <input type="hidden" id="tm-h-<%= treemap_id -%>" value="<%= heightInPercents -%>"/>
+</div>
+<div id="tm-error-<%= treemap_id -%>" class="error" style="display:none"></div>
+<div id="tm-<%= treemap_id -%>" class="treemap spacer-bottom"></div>
+
+<div style="margin: 5px 0 0 0" class="notes">
+ <div style="float: right"><span>Left click to zoom in. Right click to zoom out.</span></div>
+ <div id="tm-bc-<%= treemap_id -%>">/</div>
+</div>
+
+<script>
+ treemapContexts[<%= treemap_id -%>] = [
+ [<%= resource_id -%>, '']
+ ];
+
+ refreshTm(<%= treemap_id -%>, <%= resource_id -%>);
+</script>
end
def draw_tooltips(node)
+ ''
end
end
#
#
class Treemap::Node
- attr_accessor :id, :label, :color, :size, :bounds, :parent, :tooltip, :url, :title
+ attr_accessor :id, :label, :color, :size, :bounds, :parent, :tooltip, :url, :title, :rid, :browsable
attr_reader :children
#
@url = opts[:url]
@tooltip = opts[:tooltip]
@children = []
+ @rid = opts[:rid]
+ @browsable = opts[:browsable]
if(@id.nil?)
make_id
box.options[i].selected = 'selected';
}
}
-};
\ No newline at end of file
+};
+
+var treemapContexts = {};
+
+function addTmEvent(treemap_id, elt_index) {
+ var elt = $('tm-node-' + treemap_id + '-' + elt_index);
+ elt.oncontextmenu = function () {
+ return false
+ };
+ elt.observe('mouseup', function (event) {
+ context = treemapContexts[treemap_id];
+ onTmClick(treemap_id, event, context);
+ });
+}
+
+function onTmClick(treemap_id, event, context) {
+ if (Event.isLeftClick(event)) {
+ var link = event.findElement('a');
+ if (link != null) {
+ event.stopPropagation();
+ return false;
+ }
+
+ var elt = event.findElement('div');
+ var rid = elt.readAttribute('rid');
+ var browsable = elt.hasAttribute('b');
+ if (browsable) {
+ var label = elt.innerText || elt.textContent;
+ context.push([rid, label]);
+ refreshTm(treemap_id, rid);
+ } else {
+ openResource(rid);
+ }
+
+ } else if (Event.isRightClick(event)) {
+ if (context.length > 1) {
+ context.pop();
+ var rid = context[context.length - 1][0];
+ refreshTm(treemap_id, rid);
+ }
+ }
+}
+
+function refreshTm(treemap_id, resource_id) {
+ var size = $F('tm-size-' + treemap_id);
+ var color = $F('tm-color-' + treemap_id);
+ var width = $('tm-' + treemap_id).getWidth() - 10;
+ var height = Math.round(width * parseFloat($F('tm-h-' + treemap_id) / 100.0));
+
+ context = treemapContexts[treemap_id];
+ var output = '';
+ context.each(function (elt) {
+ output += elt[1] + ' / ';
+ });
+ $('tm-bc-' + treemap_id).innerHTML = output;
+ $('tm-loading-' + treemap_id).show();
+
+ new Ajax.Request(
+ baseUrl + '/treemap/index?id=' + treemap_id + '&width=' + width + '&height=' + height + '&size_metric=' + size + '&color_metric=' + color + '&resource=' + resource_id,
+ {asynchronous:true, evalScripts:true});
+
+ return false;
+}
+
+function openResource(key) {
+ document.location = baseUrl + '/dashboard/index/' + key;
+ return false;
+}
\ No newline at end of file
/* LAYOUT */
div#container {
height: auto !important;
+ min-width: 940px;
}
div#hd {
position: relative;
cursor: pointer;
}
-
.treemap .label {
color: #fff;
- padding: 2px;
+}
+.treemap a {
+ color: #FFF;
+ text-decoration: none;
+ font-size: 12px;
+ padding: 1px;
+}
+.treemap a:hover {
+ text-decoration: underline;
}
/* ------------------- MESSAGES ------------------- */