From a771c38d51e041fc274e567effa9e06a2566b6cf Mon Sep 17 00:00:00 2001 From: Fabrice Bellingard Date: Mon, 22 Aug 2011 18:59:58 +0200 Subject: [PATCH] SONAR-2074 Improve timeline widget - Add top and bottom "padding" inside the graph for better readibility - Display a message if only 1 snapshot available - Display the "legend" under the graph with localized metric names - Add possibility to specify widget height in the options - Select first the last snapshot values when rendering the widget - Add date label (localized) --- .../plugins/core/widgets/TimelineWidget.java | 3 +- .../plugins/core/widgets/timeline.html.erb | 81 ++++++++++++++----- .../resources/org/sonar/l10n/core.properties | 1 + .../webapp/WEB-INF/app/models/trends_chart.rb | 2 +- .../main/webapp/javascripts/protovis-sonar.js | 54 ++++++++++--- 5 files changed, 106 insertions(+), 35 deletions(-) diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimelineWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimelineWidget.java index 6faba17f939..0569f5ff6d5 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimelineWidget.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TimelineWidget.java @@ -31,7 +31,8 @@ import org.sonar.api.web.WidgetPropertyType; @WidgetProperty(key = "metric1", type = WidgetPropertyType.METRIC), @WidgetProperty(key = "metric2", type = WidgetPropertyType.METRIC), @WidgetProperty(key = "metric3", type = WidgetPropertyType.METRIC), - @WidgetProperty(key = "displayEvents", type = WidgetPropertyType.BOOLEAN) + @WidgetProperty(key = "displayEvents", type = WidgetPropertyType.BOOLEAN), + @WidgetProperty(key = "widgetHeight", type = WidgetPropertyType.INTEGER) } ) public class TimelineWidget extends AbstractRubyTemplate implements RubyRailsWidget { diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/timeline.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/timeline.html.erb index 1c2f6ffc520..a8d8d10ee45 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/timeline.html.erb +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/timeline.html.erb @@ -1,43 +1,64 @@ <%= javascript_include_tag 'protovis-sonar' %> <% metric_data_map = {} + metric_name_map = {} (1..3).each do |index| metric=widget_properties["metric#{index}"] if metric metric_data_map[metric.id] = [] + metric_name_map[metric.id] = message('metric.' + metric.name + '.name') end end + # Retrieve metric trend information options = {} from_date = dashboard_configuration.from_datetime if from_date options[:from] = from_date end - TrendsChart.time_machine_measures(@resource, metric_data_map.keys, options).each() do |trend_item| - metric_data_map[trend_item["metric_id"].to_i] << {:date => trend_item["created_at"], :value => trend_item["value"]} + metric_data_map[trend_item["metric_id"].to_i] << {:date => trend_item["created_at"], :value => trend_item["value"], :sid => trend_item["sid"]} end + # Create JS structures to print out in the HTML page js_data = "[" - metric_data_map.keys.each() do |metric_id| + js_snapshots = "[" + js_metrics = "[" + metric_data_map.keys.each_with_index() do |metric_id, index| + js_metrics += "\"" + metric_name_map[metric_id] + "\"," js_data += "[" metric_data_map[metric_id].each() do |metric_data| m_date = Time.parse(metric_data[:date]) js_data += "{\"x\":d(" - js_data += m_date.year.to_s - js_data += "," - # Need to decrease by 1 the month as the JS Date object start months at 0 (= January) - js_data += (m_date.month - 1).to_s - js_data += "," - js_data += m_date.day.to_s - js_data += "),\"y\":" - js_data += sprintf( "%0.02f", metric_data[:value]) - js_data += "}," + js_data += m_date.year.to_s + js_data += "," + # Need to decrease by 1 the month as the JS Date object start months at 0 (= January) + js_data += (m_date.month - 1).to_s + js_data += "," + js_data += m_date.day.to_s + js_data += "),\"y\":" + js_data += sprintf( "%0.02f", metric_data[:value]) + js_data += "}," + if index == 0 + # we fill the js_snapshots array (no need to do this more than once) + js_snapshots += "{\"sid\":" + js_snapshots += metric_data[:sid] + js_snapshots += ",\"d\":\"" + js_snapshots += human_short_date(m_date) + js_snapshots += "\"}," + end end js_data += "]," end js_data += "]" - + js_snapshots += "]" + js_metrics += "]" + + # And prepare translations for labels + js_translations = "{" + js_translations += "\"date\":\"" + message("date") + "\"" + js_translations += "}" + %> <% if widget_properties["chartTitle"] %> @@ -45,13 +66,29 @@ <% end %> -
- - \ No newline at end of file +<% end %> \ No newline at end of file diff --git a/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties b/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties index f61dd520ddd..a61de34dc63 100644 --- a/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties +++ b/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties @@ -510,6 +510,7 @@ widget.size.paragraphs.suffix=\ paragraphs widget.timeline.name=Timeline widget.timeline.description=Displays up to 3 metrics on a history chart. +widget.timeline.timeline_not_displayed=The timeline won't be displayed as currently only 1 snapshot is available. widget.ckjm.name=Chidamber & Kemerer widget.ckjm.description=Reports on LCOM4 and RFC average and distribution. diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/trends_chart.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/trends_chart.rb index 00d2a31ab02..8c60ee3eed5 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/trends_chart.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/trends_chart.rb @@ -41,7 +41,7 @@ class TrendsChart def self.time_machine_measures(resource, metric_ids, options={}) unless metric_ids.empty? - sql= "select s.created_at as created_at, m.value as value, m.metric_id as metric_id " + + sql= "select s.created_at as created_at, m.value as value, m.metric_id as metric_id, s.id as sid " + " from project_measures m LEFT OUTER JOIN snapshots s ON s.id=m.snapshot_id " + " where m.rule_id is null " + " and s.status=? " + diff --git a/sonar-server/src/main/webapp/javascripts/protovis-sonar.js b/sonar-server/src/main/webapp/javascripts/protovis-sonar.js index aa6e34e2ac9..1de76f4c5ac 100755 --- a/sonar-server/src/main/webapp/javascripts/protovis-sonar.js +++ b/sonar-server/src/main/webapp/javascripts/protovis-sonar.js @@ -2,29 +2,54 @@ window.SonarWidgets = {} SonarWidgets.Timeline = function (divId) { this.wDivId = divId; + this.wHeight; this.wData; + this.wSnapshots; + this.wMetrics; + this.wTranslations; + this.height = function(height) { + this.wHeight = height; + return this; + } this.data = function(data) { this.wData = data; return this; } + this.snapshots = function(snapshots) { + this.wSnapshots = snapshots; + return this; + } + this.metrics = function(metrics) { + this.wMetrics = metrics; + return this; + } + this.translations = function(translations) { + this.wTranslations = translations; + return this; + } } SonarWidgets.Timeline.prototype.render = function() { + var trendData = this.wData; + var metrics = this.wMetrics; + var snapshots = this.wSnapshots; + var translations = this.wTranslations; var widgetDiv = document.getElementById(this.wDivId); /* Sizing and scales. */ + var footerHeight = 30 + this.wMetrics.size() * 12; var w = widgetDiv.parentNode.clientWidth - 60, - h = 300 - 25, + h = (this.wHeight == null ? 250 : this.wHeight) + footerHeight - 5, S=2; var x = pv.Scale.linear(pv.blend(pv.map(data, function(d) {return d;})), function(d) {return d.x}).range(0, w); var y = new Array(data.length); for(var i = 0; i < data.length; i++){ - y[i]=pv.Scale.linear(data[i], function(d) {return d.y;}).range(0, h) + y[i]=pv.Scale.linear(data[i], function(d) {return d.y;}).range(10, h-10) } var interpolate = "linear"; /* cardinal or linear */ - var idx = -1; + var idx = this.wData[0].size() - 1; /* The root panel. */ var vis = new pv.Panel() @@ -33,7 +58,7 @@ SonarWidgets.Timeline.prototype.render = function() { .height(h) .left(30) .right(20) - .bottom(20) + .bottom(footerHeight) .top(5) .strokeStyle("#CCC"); @@ -61,7 +86,7 @@ SonarWidgets.Timeline.prototype.render = function() { /* A panel for each data series. */ var panel = vis.add(pv.Panel) - .data(this.wData); + .data(trendData); /* The line. */ var line = panel.add(pv.Line) @@ -71,7 +96,7 @@ SonarWidgets.Timeline.prototype.render = function() { .interpolate(function() {return interpolate;}) .lineWidth(2); - /* The mouseover dots and label. */ + /* The mouseover dots and label in footer. */ line.add(pv.Dot) .visible(function() {return idx >= 0;}) .data(function(d) {return [d[idx]];}) @@ -80,12 +105,19 @@ SonarWidgets.Timeline.prototype.render = function() { .size(20) .lineWidth(1) .add(pv.Dot) - .left(10) - .bottom(function() {return this.parent.index * 12 + 10;}) + .left(150) + .bottom(function() {return 0 - 30 - this.parent.index * 12;}) .anchor("right").add(pv.Label) - .text(function(d) {return d.y.toFixed(2);}); - - + .font("12px Arial,Helvetica,sans-serif") + .text(function(d) {return metrics[this.parent.index] + ": " + d.y.toFixed(2);}); + + /* The date of the selected dot in footer. */ + vis.add(pv.Label) + .left(0) + .bottom(-36) + .font("12px Arial,Helvetica,sans-serif") + .text(function() {return translations.date + ": " + snapshots[idx].d}); + /* An invisible bar to capture events (without flickering). */ vis.add(pv.Bar) .fillStyle("rgba(0,0,0,.001)") -- 2.39.5