From 2ae51a4c28f5ccfb85540b7f52007ea1f0843ccf Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Sun, 27 Oct 2013 13:48:40 +0100 Subject: SONAR-4817 Replace timeline code base --- sonar-server/pom.xml | 3 +- .../WEB-INF/app/views/layouts/_head.html.erb | 3 +- .../src/main/webapp/javascripts/bubble-chart.js | 421 --------------------- .../src/main/webapp/javascripts/protovis-sonar.js | 241 ------------ .../webapp/javascripts/widgets/bubble-chart.js | 421 +++++++++++++++++++++ .../main/webapp/javascripts/widgets/timeline.js | 399 +++++++++++++++++++ sonar-server/src/main/webapp/stylesheets/style.css | 62 +++ 7 files changed, 886 insertions(+), 664 deletions(-) delete mode 100644 sonar-server/src/main/webapp/javascripts/bubble-chart.js create mode 100644 sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js create mode 100644 sonar-server/src/main/webapp/javascripts/widgets/timeline.js (limited to 'sonar-server') diff --git a/sonar-server/pom.xml b/sonar-server/pom.xml index 3cac776afd9..01bec19ba87 100644 --- a/sonar-server/pom.xml +++ b/sonar-server/pom.xml @@ -248,7 +248,8 @@ **/jquery-ui.min.js **/third-party/d3.v3.min.js **/select2.min.js - **/bubble-chart.js + **/widgets/bubble-chart.js + **/widgets/timeline.js **/application-min.js **/dashboard-min.js **/duplication-min.js diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb index 367e0afaf75..18726222b8a 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb @@ -40,7 +40,8 @@ <%= javascript_include_tag 'select2.min' %> <%= javascript_include_tag 'protovis' %> <%= javascript_include_tag 'protovis-sonar' %> - <%= javascript_include_tag 'bubble-chart' %> + <%= javascript_include_tag 'widgets/bubble-chart' %> + <%= javascript_include_tag 'widgets/timeline' %> <%= javascript_include_tag 'application' %> <%= javascript_include_tag 'dashboard' %> <%= javascript_include_tag 'duplication' %> diff --git a/sonar-server/src/main/webapp/javascripts/bubble-chart.js b/sonar-server/src/main/webapp/javascripts/bubble-chart.js deleted file mode 100644 index 9f31d662f02..00000000000 --- a/sonar-server/src/main/webapp/javascripts/bubble-chart.js +++ /dev/null @@ -1,421 +0,0 @@ -/*global d3:false, baseUrl:false */ - -window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets; - -(function () { - - window.SonarWidgets.BubbleChart = function () { - // Set default values - this._data = []; - this._metrics = []; - this._width = window.SonarWidgets.BubbleChart.defaults.width; - this._height = window.SonarWidgets.BubbleChart.defaults.height; - this._margin = window.SonarWidgets.BubbleChart.defaults.margin; - this._xLog = window.SonarWidgets.BubbleChart.defaults.xLog; - this._yLog = window.SonarWidgets.BubbleChart.defaults.yLog; - this._bubbleColor = window.SonarWidgets.BubbleChart.defaults.bubbleColor; - this._bubbleColorUndefined = window.SonarWidgets.BubbleChart.defaults.bubbleColorUndefined; - - // Export global variables - this.data = function (_) { - return param.call(this, '_data', _); - }; - - this.metrics = function (_) { - return param.call(this, '_metrics', _); - }; - - this.width = function (_) { - return param.call(this, '_width', _); - }; - - this.height = function (_) { - return param.call(this, '_height', _); - }; - - this.margin = function (_) { - return param.call(this, '_margin', _); - }; - - this.xLog = function (_) { - return param.call(this, '_xLog', _); - }; - - this.yLog = function (_) { - return param.call(this, '_yLog', _); - }; - - this.bubbleColor = function (_) { - return param.call(this, '_bubbleColor', _); - }; - - this.bubbleColorUndefined = function (_) { - return param.call(this, '_bubbleColorUndefined', _); - }; - }; - - window.SonarWidgets.BubbleChart.prototype.render = function (container) { - var widget = this, - containerS = container; - - container = d3.select(container); - - this.width(container.property('offsetWidth')); - - this.svg = container.append('svg'); - this.gWrap = this.svg.append('g'); - - this.gxAxis = this.gWrap.append('g'); - this.gyAxis = this.gWrap.append('g'); - - this.gGrid = this.gWrap.append('g'); - this.gxGrid = this.gGrid.append('g'); - this.gyGrid = this.gGrid.append('g'); - - this.plotWrap = this.gWrap.append('g'); - - this.infoWrap = this.gWrap.append('g'); - this.infoName = this.infoWrap.append('text'); - this.infoMetrics = this.infoWrap.append('text'); - - this.gWrap - .attr('transform', trans(this.margin().left, this.margin().top)); - - - // Configure scales - this.x = this.xLog() ? d3.scale.log() : d3.scale.linear(); - this.y = this.yLog() ? d3.scale.log() : d3.scale.linear(); - this.size = d3.scale.linear(); - - this.x - .domain(d3.extent(this.data(), function (d) { - return d.xMetric; - })) - .range([0, this.availableWidth]); - - this.y - .domain(d3.extent(this.data(), function (d) { - return d.yMetric; - })) - .range([this.availableHeight, 0]); - - this.size - .domain(d3.extent(this.data(), function (d) { - return d.sizeMetric; - })) - .range([10, 50]); - - - // Create bubbles - this.items = this.plotWrap.selectAll('.item') - .data(this.data()); - - - // Render bubbles - this.items.enter().append('g') - .attr('class', 'item') - .attr('name', function (d) { - return d.longName; - }) - .style('cursor', 'pointer') - .append('circle') - .attr('r', function (d) { - return widget.size(d.sizeMetric); - }) - .style('fill', function (d) { - return d.sizeMetricFormatted !== '-' ? - widget.bubbleColor() : - widget.bubbleColorUndefined(); - }) - .style('fill-opacity', 0.2) - .style('stroke', function (d) { - return d.sizeMetricFormatted !== '-' ? - widget.bubbleColor() : - widget.bubbleColorUndefined(); - }) - .style('transition', 'all 0.2s ease'); - - this.items.exit().remove(); - - this.items.sort(function (a, b) { - return b.sizeMetric - a.sizeMetric; - }); - - - // Set event listeners - this.items - .on('click', function (d) { - var url = baseUrl + '/resource/index/' + d.key + '?display_title=true&metric=ncloc'; - window.open(url, '', 'height=800,width=900,scrollbars=1,resizable=1'); - }) - .on('mouseenter', function (d) { - d3.select(this).select('circle') - .style('fill-opacity', 0.8); - - widget.infoName.text(d.longName); - widget.infoMetrics.text( - widget.metrics().x + ': ' + d.xMetricFormatted + '; ' + - widget.metrics().y + ': ' + d.yMetricFormatted + '; ' + - widget.metrics().size + ': ' + d.sizeMetricFormatted); - }) - .on('mouseleave', function () { - d3.select(this).select('circle') - .style('fill-opacity', 0.2); - - widget.infoName.text(''); - widget.infoMetrics.text(''); - }); - - - // Configure axis - // X - this.xAxis = d3.svg.axis() - .scale(widget.x) - .orient('bottom'); - - this.gxAxisLabel = this.gxAxis.append('text') - .text(this.metrics().x) - .style('font-weight', 'bold') - .style('text-anchor', 'middle'); - - - // Y - this.yAxis = d3.svg.axis() - .scale(widget.y) - .orient('left'); - - this.gyAxis.attr('transform', trans(60 - this.margin().left, 0)); - - this.gyAxisLabel = this.gyAxis.append('text') - .text(this.metrics().y) - .style('font-weight', 'bold') - .style('text-anchor', 'middle'); - - - // Configure grid - this.gxGridLines = this.gxGrid.selectAll('line').data(widget.x.ticks()).enter() - .append('line'); - - this.gyGridLines = this.gyGrid.selectAll('line').data(widget.y.ticks()).enter() - .append('line'); - - this.gGrid.selectAll('line') - .style('stroke', '#000') - .style('stroke-opacity', 0.25); - - - // Configure info placeholders - this.infoWrap - .attr('transform', trans(-this.margin().left, -this.margin().top + 20)); - - this.infoName - .style('text-anchor', 'start') - .style('font-weight', 'bold'); - - this.infoMetrics - .attr('transform', trans(0, 20)); - - - // Update widget - this.update(containerS); - - return this; - }; - - - - window.SonarWidgets.BubbleChart.prototype.update = function(container) { - container = d3.select(container); - - var widget = this, - width = container.property('offsetWidth'); - - this.width(width > 100 ? width : 100); - - - // Update svg canvas - this.svg - .attr('width', this.width()) - .attr('height', this.height()); - - - // Update available size - this.availableWidth = this.width() - this.margin().left - this.margin().right; - this.availableHeight = this.height() - this.margin().top - this.margin().bottom; - - - // Update scales - this.x - .domain(d3.extent(this.data(), function (d) { - return d.xMetric; - })) - .range([0, this.availableWidth]); - - this.y - .domain(d3.extent(this.data(), function (d) { - return d.yMetric; - })) - .range([this.availableHeight, 0]); - - - // Avoid zero values when using log scale - if (this.xLog) { - var xDomain = this.x.domain(); - this.x - .domain([xDomain[0] > 0 ? xDomain[0] : 0.1, xDomain[1]]) - .clamp(true); - } - - if (this.yLog) { - var yDomain = this.y.domain(); - this.y - .domain([yDomain[0] > 0 ? yDomain[0] : 0.1, yDomain[1]]) - .clamp(true); - } - - - // Adjust the scale domain so the circles don't cross the bounds - // X - var minX = d3.min(this.data(), function (d) { - return widget.x(d.xMetric) - widget.size(d.sizeMetric); - }), - maxX = d3.max(this.data(), function (d) { - return widget.x(d.xMetric) + widget.size(d.sizeMetric); - }), - dMinX = this.x.range()[0] - minX, - dMaxX = maxX - this.x.range()[1]; - this.x.range([dMinX, this.availableWidth - dMaxX]); - - // Y - var minY = d3.min(this.data(), function (d) { - return widget.y(d.yMetric) - widget.size(d.sizeMetric); - }), - maxY = d3.max(this.data(), function (d) { - return widget.y(d.yMetric) + widget.size(d.sizeMetric); - }), - dMinY = this.y.range()[1] - minY, - dMaxY = maxY - this.y.range()[0]; - this.y.range([this.availableHeight - dMaxY, dMinY]); - - - // Format improvement for log scales - // X - if (this.xLog()) { - this.xAxis.tickFormat(function (d) { - var ticksCount = widget.availableWidth / 50; - return widget.x.tickFormat(ticksCount, d3.format(",d"))(d); - }); - } - - // Y - if (this.yLog()) { - this.yAxis.tickFormat(function (d) { - var ticksCount = widget.availableHeight / 30; - return widget.y.tickFormat(ticksCount, d3.format(",d"))(d); - }); - } - - - // Make scale's domains nice - this.x.nice(); - this.y.nice(); - - - // Update bubbles position - this.items - .transition() - .attr('transform', function (d) { - return trans(widget.x(d.xMetric), widget.y(d.yMetric)); - }); - - - // Update axis - // X - this.gxAxis.attr('transform', trans(0, this.availableHeight + this.margin().bottom - 40)); - - this.gxAxis.transition().call(this.xAxis); - - this.gxAxis.selectAll('path') - .style('fill', 'none') - .style('stroke', '#444'); - - this.gxAxis.selectAll('text') - .style('fill', '#444'); - - this.gxAxisLabel - .attr('transform', trans(this.availableWidth / 2, 35)); - - // Y - this.gyAxis.transition().call(this.yAxis); - - this.gyAxis.selectAll('path') - .style('fill', 'none') - .style('stroke', '#444'); - - this.gyAxis.selectAll('text') - .style('fill', '#444'); - - this.gyAxisLabel - .attr('transform', trans(-45, this.availableHeight / 2) + ' rotate(-90)'); - - - // Update grid - this.gxGridLines - .transition() - .attr({ - x1: function (d) { - return widget.x(d); - }, - x2: function (d) { - return widget.x(d); - }, - y1: widget.y.range()[0], - y2: widget.y.range()[1] - }); - - this.gyGridLines - .transition() - .attr({ - x1: widget.x.range()[0], - x2: widget.x.range()[1], - y1: function (d) { - return widget.y(d); - }, - y2: function (d) { - return widget.y(d); - } - }); - }; - - - - window.SonarWidgets.BubbleChart.defaults = { - width: 350, - height: 150, - margin: { top: 60, right: 10, bottom: 50, left: 70 }, - xLog: false, - yLog: false, - bubbleColor: '#4b9fd5', - bubbleColorUndefined: '#b3b3b3' - }; - - - - // Some helper functions - - // Gets or sets parameter - function param(name, value) { - if (value == null) { - return this[name]; - } else { - this[name] = value; - return this; - } - } - - // Helper for create the translate(x, y) string - function trans(left, top) { - return 'translate(' + left + ', ' + top + ')'; - } - -})(); diff --git a/sonar-server/src/main/webapp/javascripts/protovis-sonar.js b/sonar-server/src/main/webapp/javascripts/protovis-sonar.js index f8a79167ff1..a90a2e60528 100644 --- a/sonar-server/src/main/webapp/javascripts/protovis-sonar.js +++ b/sonar-server/src/main/webapp/javascripts/protovis-sonar.js @@ -244,244 +244,3 @@ SonarWidgets.StackArea.prototype.render = function () { vis.render(); }; - - -//******************* TIMELINE CHART ******************* // -/* - * Displays the evolution of metrics on a line chart, displaying related events. - * - * Parameters of the Timeline class: - * - data: array of arrays, each containing maps {x,y,yl} where x is a (JS) date, y is a number value (representing a metric value at - * a given time), and yl the localized value of y. The {x,y, yl} maps must be sorted by ascending date. - * - metrics: array of metric names. The order is important as it defines which array of the "data" parameter represents which metric. - * - snapshots: array of maps {sid,d} where sid is the snapshot id and d is the locale-formatted date of the snapshot. The {sid,d} - * maps must be sorted by ascending date. - * - events: array of maps {sid,d,l[{n}]} where sid is the snapshot id corresponding to an event, d is the (JS) date of the event, and l - * is an array containing the different event names for this date. - * - height: height of the chart area (notice header excluded). Defaults to 80. - * - * Example: displays 2 metrics: - * - - function d(y,m,d,h,min,s) { - return new Date(y,m,d,h,min,s); - } - var data = [ - [{x:d(2011,5,15,0,1,0),y:912.00,yl:"912"},{x:d(2011,6,21,0,1,0),y:152.10,yl:"152.10"}], - [{x:d(2011,5,15,0,1,0),y:52.20,yi:"52.20"},{x:d(2011,6,21,0,1,0),y:1452.10,yi:"1,452.10"}] - ]; - var metrics = ["Lines of code","Rules compliance"]; - var snapshots = [{sid:1,d:"June 15, 2011 00:01"},{sid:30,d:"July 21, 2011 00:01"}]; - var events = [ - {sid:1,d:d(2011,5,15,0,1,0),l:[{n:"0.6-SNAPSHOT"},{n:"Sun checks"}]}, - {sid:30,d:d(2011,6,21,0,1,0),l:[{n:"0.7-SNAPSHOT"}]} - ]; - - var timeline = new SonarWidgets.Timeline('timeline-chart-20') - .height(160) - .data(data) - .snapshots(snapshots) - .metrics(metrics) - .events(events); - timeline.render(); - - * - */ - -SonarWidgets.Timeline = function (divId) { - this.wDivId = divId; - this.wHeight; - this.wData; - this.wSnapshots; - this.wMetrics; - this.wEvents; - 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.events = function (events) { - this.wEvents = events; - return this; - }; -}; - -SonarWidgets.Timeline.prototype.render = function () { - - var trendData = this.wData; - var metrics = this.wMetrics; - var snapshots = this.wSnapshots; - var events = this.wEvents; - - var widgetDiv = $(this.wDivId); - var headerFont = "10.5px Arial,Helvetica,sans-serif"; - - /* Sizing and scales. */ - var headerHeight = 4 + Math.max(this.wMetrics.size(), events ? 2 : 1) * 18; - var w = widgetDiv.getOffsetParent().getWidth() - 60; - var h = (this.wHeight == null || this.wHeight <= 0 ? 80 : this.wHeight) - 40; - var yMaxHeight = h - headerHeight; - - var x = pv.Scale.linear(pv.blend(pv.map(trendData, function (d) { - return d; - })), - function (d) { - return d.x; - }).range(0, w); - var y = new Array(trendData.size()); - for (var i = 0; i < trendData.size(); i++) { - y[i] = pv.Scale.linear(trendData[i], - function (d) { - return d.y; - }).range(20, yMaxHeight); - } - var interpolate = "linear"; - /* cardinal or linear */ - var idx = trendData[0].size() - 1; - - /* The root panel. */ - var vis = new pv.Panel() - .canvas(widgetDiv) - .width(w) - .height(h) - .left(20) - .right(20) - .bottom(30) - .top(5) - .strokeStyle("#CCC"); - - /* X-axis */ - vis.add(pv.Rule) - .data(x.ticks()) - .left(x) - .bottom(-10) - .height(10) - .anchor("bottom") - .add(pv.Label) - .text(x.tickFormat); - - /* A panel for each data series. */ - var panel = vis.add(pv.Panel) - .data(trendData); - - /* The line. */ - var line = panel.add(pv.Line) - .data(function (array) { - return array; - }) - .left(function (d) { - return x(d.x); - }) - .bottom(function (d) { - var yAxis = y[this.parent.index](d.y); - return isNaN(yAxis) ? yMaxHeight : yAxis; - }) - .interpolate(function () { - return interpolate; - }) - .lineWidth(2); - - /* The mouseover dots and label in header. */ - line.add(pv.Dot) - .data(function (d) { - return [d[idx]]; - }) - .fillStyle(function () { - return line.strokeStyle(); - }) - .strokeStyle("#000") - .size(20) - .lineWidth(1) - .add(pv.Dot) - .radius(3) - .left(10) - .top(function () { - return 10 + this.parent.index * 14; - }) - .anchor("right").add(pv.Label) - .font(headerFont) - .text(function (d) { - return metrics[this.parent.index] + ": " + d.yl; - }); - - /* The date of the selected dot in the header. */ - vis.add(pv.Label) - .left(w / 2) - .top(16) - .font(headerFont) - .text(function () { - return snapshots[idx].d; - }); - - /* The event labels */ - if (events) { - eventColor = "rgba(75,159,213,1)"; - eventHoverColor = "rgba(202,227,242,1)"; - vis.add(pv.Line) - .strokeStyle("rgba(0,0,0,.001)") - .data(events) - .left(function (e) { - return x(e.d); - }) - .bottom(0) - .anchor("top") - .add(pv.Dot) - .bottom(-6) - .shape("triangle") - .angle(pv.radians(180)) - .strokeStyle("grey") - .fillStyle(function (e) { - return e.sid == snapshots[idx].sid ? eventHoverColor : eventColor; - }) - .add(pv.Dot) - .radius(3) - .visible(function (e) { - return e.sid == snapshots[idx].sid; - }) - .left(w / 2 + 8) - .top(24) - .shape("triangle") - .fillStyle(function (e) { - return e.sid == snapshots[idx].sid ? eventHoverColor : eventColor; - }) - .strokeStyle("grey") - .anchor("right") - .add(pv.Label) - .font(headerFont) - .text(function (e) { - return e.l[0].n + ( e.l[1] ? " (... +" + (e.l.size() - 1) + ")" : ""); - }); - } - - /* An invisible bar to capture events (without flickering). */ - vis.add(pv.Bar) - .fillStyle("rgba(0,0,0,.001)") - .width(w + 30) - .height(h + 30) - .event("mouseout", function () { - i = -1; - return vis; - }) - .event("mousemove", function () { - var mx = x.invert(vis.mouse().x); - idx = pv.search(trendData[0].map(function (d) { - return d.x; - }), mx); - idx = idx < 0 ? (-idx - 2) : idx; - idx = idx < 0 ? 0 : idx; - return vis; - }); - - vis.render(); -}; diff --git a/sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js b/sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js new file mode 100644 index 00000000000..d35366559c3 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js @@ -0,0 +1,421 @@ +/*global d3:false, baseUrl:false */ + +window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets; + +(function () { + + window.SonarWidgets.BubbleChart = function () { + // Set default values + this._data = []; + this._metrics = []; + this._width = window.SonarWidgets.BubbleChart.defaults.width; + this._height = window.SonarWidgets.BubbleChart.defaults.height; + this._margin = window.SonarWidgets.BubbleChart.defaults.margin; + this._xLog = window.SonarWidgets.BubbleChart.defaults.xLog; + this._yLog = window.SonarWidgets.BubbleChart.defaults.yLog; + this._bubbleColor = window.SonarWidgets.BubbleChart.defaults.bubbleColor; + this._bubbleColorUndefined = window.SonarWidgets.BubbleChart.defaults.bubbleColorUndefined; + + // Export global variables + this.data = function (_) { + return param.call(this, '_data', _); + }; + + this.metrics = function (_) { + return param.call(this, '_metrics', _); + }; + + this.width = function (_) { + return param.call(this, '_width', _); + }; + + this.height = function (_) { + return param.call(this, '_height', _); + }; + + this.margin = function (_) { + return param.call(this, '_margin', _); + }; + + this.xLog = function (_) { + return param.call(this, '_xLog', _); + }; + + this.yLog = function (_) { + return param.call(this, '_yLog', _); + }; + + this.bubbleColor = function (_) { + return param.call(this, '_bubbleColor', _); + }; + + this.bubbleColorUndefined = function (_) { + return param.call(this, '_bubbleColorUndefined', _); + }; + }; + + window.SonarWidgets.BubbleChart.prototype.render = function (container) { + var widget = this, + containerS = container; + + container = d3.select(container); + + this.width(container.property('offsetWidth')); + + this.svg = container.append('svg'); + this.gWrap = this.svg.append('g'); + + this.gxAxis = this.gWrap.append('g'); + this.gyAxis = this.gWrap.append('g'); + + this.gGrid = this.gWrap.append('g'); + this.gxGrid = this.gGrid.append('g'); + this.gyGrid = this.gGrid.append('g'); + + this.plotWrap = this.gWrap.append('g'); + + this.infoWrap = this.gWrap.append('g'); + this.infoDate = this.infoWrap.append('text'); + this.infoMetrics = this.infoWrap.append('text'); + + this.gWrap + .attr('transform', trans(this.margin().left, this.margin().top)); + + + // Configure scales + this.x = this.xLog() ? d3.scale.log() : d3.scale.linear(); + this.y = this.yLog() ? d3.scale.log() : d3.scale.linear(); + this.size = d3.scale.linear(); + + this.x + .domain(d3.extent(this.data(), function (d) { + return d.xMetric; + })) + .range([0, this.availableWidth]); + + this.y + .domain(d3.extent(this.data(), function (d) { + return d.yMetric; + })) + .range([this.availableHeight, 0]); + + this.size + .domain(d3.extent(this.data(), function (d) { + return d.sizeMetric; + })) + .range([10, 50]); + + + // Create bubbles + this.items = this.plotWrap.selectAll('.item') + .data(this.data()); + + + // Render bubbles + this.items.enter().append('g') + .attr('class', 'item') + .attr('name', function (d) { + return d.longName; + }) + .style('cursor', 'pointer') + .append('circle') + .attr('r', function (d) { + return widget.size(d.sizeMetric); + }) + .style('fill', function (d) { + return d.sizeMetricFormatted !== '-' ? + widget.bubbleColor() : + widget.bubbleColorUndefined(); + }) + .style('fill-opacity', 0.2) + .style('stroke', function (d) { + return d.sizeMetricFormatted !== '-' ? + widget.bubbleColor() : + widget.bubbleColorUndefined(); + }) + .style('transition', 'all 0.2s ease'); + + this.items.exit().remove(); + + this.items.sort(function (a, b) { + return b.sizeMetric - a.sizeMetric; + }); + + + // Set event listeners + this.items + .on('click', function (d) { + var url = baseUrl + '/resource/index/' + d.key + '?display_title=true&metric=ncloc'; + window.open(url, '', 'height=800,width=900,scrollbars=1,resizable=1'); + }) + .on('mouseenter', function (d) { + d3.select(this).select('circle') + .style('fill-opacity', 0.8); + + widget.infoDate.text(d.longName); + widget.infoMetrics.text( + widget.metrics().x + ': ' + d.xMetricFormatted + '; ' + + widget.metrics().y + ': ' + d.yMetricFormatted + '; ' + + widget.metrics().size + ': ' + d.sizeMetricFormatted); + }) + .on('mouseleave', function () { + d3.select(this).select('circle') + .style('fill-opacity', 0.2); + + widget.infoDate.text(''); + widget.infoMetrics.text(''); + }); + + + // Configure axis + // X + this.xAxis = d3.svg.axis() + .scale(widget.x) + .orient('bottom'); + + this.gxAxisLabel = this.gxAxis.append('text') + .text(this.metrics().x) + .style('font-weight', 'bold') + .style('text-anchor', 'middle'); + + + // Y + this.yAxis = d3.svg.axis() + .scale(widget.y) + .orient('left'); + + this.gyAxis.attr('transform', trans(60 - this.margin().left, 0)); + + this.gyAxisLabel = this.gyAxis.append('text') + .text(this.metrics().y) + .style('font-weight', 'bold') + .style('text-anchor', 'middle'); + + + // Configure grid + this.gxGridLines = this.gxGrid.selectAll('line').data(widget.x.ticks()).enter() + .append('line'); + + this.gyGridLines = this.gyGrid.selectAll('line').data(widget.y.ticks()).enter() + .append('line'); + + this.gGrid.selectAll('line') + .style('stroke', '#000') + .style('stroke-opacity', 0.25); + + + // Configure info placeholders + this.infoWrap + .attr('transform', trans(-this.margin().left, -this.margin().top + 20)); + + this.infoDate + .style('text-anchor', 'start') + .style('font-weight', 'bold'); + + this.infoMetrics + .attr('transform', trans(0, 20)); + + + // Update widget + this.update(containerS); + + return this; + }; + + + + window.SonarWidgets.BubbleChart.prototype.update = function(container) { + container = d3.select(container); + + var widget = this, + width = container.property('offsetWidth'); + + this.width(width > 100 ? width : 100); + + + // Update svg canvas + this.svg + .attr('width', this.width()) + .attr('height', this.height()); + + + // Update available size + this.availableWidth = this.width() - this.margin().left - this.margin().right; + this.availableHeight = this.height() - this.margin().top - this.margin().bottom; + + + // Update scales + this.x + .domain(d3.extent(this.data(), function (d) { + return d.xMetric; + })) + .range([0, this.availableWidth]); + + this.y + .domain(d3.extent(this.data(), function (d) { + return d.yMetric; + })) + .range([this.availableHeight, 0]); + + + // Avoid zero values when using log scale + if (this.xLog) { + var xDomain = this.x.domain(); + this.x + .domain([xDomain[0] > 0 ? xDomain[0] : 0.1, xDomain[1]]) + .clamp(true); + } + + if (this.yLog) { + var yDomain = this.y.domain(); + this.y + .domain([yDomain[0] > 0 ? yDomain[0] : 0.1, yDomain[1]]) + .clamp(true); + } + + + // Adjust the scale domain so the circles don't cross the bounds + // X + var minX = d3.min(this.data(), function (d) { + return widget.x(d.xMetric) - widget.size(d.sizeMetric); + }), + maxX = d3.max(this.data(), function (d) { + return widget.x(d.xMetric) + widget.size(d.sizeMetric); + }), + dMinX = this.x.range()[0] - minX, + dMaxX = maxX - this.x.range()[1]; + this.x.range([dMinX, this.availableWidth - dMaxX]); + + // Y + var minY = d3.min(this.data(), function (d) { + return widget.y(d.yMetric) - widget.size(d.sizeMetric); + }), + maxY = d3.max(this.data(), function (d) { + return widget.y(d.yMetric) + widget.size(d.sizeMetric); + }), + dMinY = this.y.range()[1] - minY, + dMaxY = maxY - this.y.range()[0]; + this.y.range([this.availableHeight - dMaxY, dMinY]); + + + // Format improvement for log scales + // X + if (this.xLog()) { + this.xAxis.tickFormat(function (d) { + var ticksCount = widget.availableWidth / 50; + return widget.x.tickFormat(ticksCount, d3.format(",d"))(d); + }); + } + + // Y + if (this.yLog()) { + this.yAxis.tickFormat(function (d) { + var ticksCount = widget.availableHeight / 30; + return widget.y.tickFormat(ticksCount, d3.format(",d"))(d); + }); + } + + + // Make scale's domains nice + this.x.nice(); + this.y.nice(); + + + // Update bubbles position + this.items + .transition() + .attr('transform', function (d) { + return trans(widget.x(d.xMetric), widget.y(d.yMetric)); + }); + + + // Update axis + // X + this.gxAxis.attr('transform', trans(0, this.availableHeight + this.margin().bottom - 40)); + + this.gxAxis.transition().call(this.xAxis); + + this.gxAxis.selectAll('path') + .style('fill', 'none') + .style('stroke', '#444'); + + this.gxAxis.selectAll('text') + .style('fill', '#444'); + + this.gxAxisLabel + .attr('transform', trans(this.availableWidth / 2, 35)); + + // Y + this.gyAxis.transition().call(this.yAxis); + + this.gyAxis.selectAll('path') + .style('fill', 'none') + .style('stroke', '#444'); + + this.gyAxis.selectAll('text') + .style('fill', '#444'); + + this.gyAxisLabel + .attr('transform', trans(-45, this.availableHeight / 2) + ' rotate(-90)'); + + + // Update grid + this.gxGridLines + .transition() + .attr({ + x1: function (d) { + return widget.x(d); + }, + x2: function (d) { + return widget.x(d); + }, + y1: widget.y.range()[0], + y2: widget.y.range()[1] + }); + + this.gyGridLines + .transition() + .attr({ + x1: widget.x.range()[0], + x2: widget.x.range()[1], + y1: function (d) { + return widget.y(d); + }, + y2: function (d) { + return widget.y(d); + } + }); + }; + + + + window.SonarWidgets.BubbleChart.defaults = { + width: 350, + height: 150, + margin: { top: 60, right: 10, bottom: 50, left: 70 }, + xLog: false, + yLog: false, + bubbleColor: '#4b9fd5', + bubbleColorUndefined: '#b3b3b3' + }; + + + + // Some helper functions + + // Gets or sets parameter + function param(name, value) { + if (value == null) { + return this[name]; + } else { + this[name] = value; + return this; + } + } + + // Helper for create the translate(x, y) string + function trans(left, top) { + return 'translate(' + left + ', ' + top + ')'; + } + +})(); diff --git a/sonar-server/src/main/webapp/javascripts/widgets/timeline.js b/sonar-server/src/main/webapp/javascripts/widgets/timeline.js new file mode 100644 index 00000000000..16052f96c71 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/widgets/timeline.js @@ -0,0 +1,399 @@ +//******************* TIMELINE CHART ******************* // +/* + * Displays the evolution of metrics on a line chart, displaying related events. + * + * Parameters of the Timeline class: + * - data: array of arrays, each containing maps {x,y,yl} where x is a (JS) date, y is a number value (representing a metric value at + * a given time), and yl the localized value of y. The {x,y, yl} maps must be sorted by ascending date. + * - metrics: array of metric names. The order is important as it defines which array of the "data" parameter represents which metric. + * - snapshots: array of maps {sid,d} where sid is the snapshot id and d is the locale-formatted date of the snapshot. The {sid,d} + * maps must be sorted by ascending date. + * - events: array of maps {sid,d,l[{n}]} where sid is the snapshot id corresponding to an event, d is the (JS) date of the event, and l + * is an array containing the different event names for this date. + * - height: height of the chart area (notice header excluded). Defaults to 80. + * + * Example: displays 2 metrics: + * + + function d(y,m,d,h,min,s) { + return new Date(y,m,d,h,min,s); + } + var data = [ + [{x:d(2011,5,15,0,1,0),y:912.00,yl:"912"},{x:d(2011,6,21,0,1,0),y:152.10,yl:"152.10"}], + [{x:d(2011,5,15,0,1,0),y:52.20,yi:"52.20"},{x:d(2011,6,21,0,1,0),y:1452.10,yi:"1,452.10"}] + ]; + var metrics = ["Lines of code","Rules compliance"]; + var snapshots = [{sid:1,d:"June 15, 2011 00:01"},{sid:30,d:"July 21, 2011 00:01"}]; + var events = [ + {sid:1,d:d(2011,5,15,0,1,0),l:[{n:"0.6-SNAPSHOT"},{n:"Sun checks"}]}, + {sid:30,d:d(2011,6,21,0,1,0),l:[{n:"0.7-SNAPSHOT"}]} + ]; + + var timeline = new SonarWidgets.Timeline('timeline-chart-20') + .height(160) + .data(data) + .snapshots(snapshots) + .metrics(metrics) + .events(events); + timeline.render(); + + * + */ + + +/*global d3:false*/ + +window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets; + +(function () { + + window.SonarWidgets.Timeline = function (container) { + + // Ensure container is html id + if (container.indexOf('#') !== 0) { + container = '#' + container; + } + + this.container = d3.select(container); + + // Set default values + this._data = []; + this._metrics = []; + this._snapshots = []; + this._events = []; + this._width = window.SonarWidgets.Timeline.defaults.width; + this._height = window.SonarWidgets.Timeline.defaults.height; + this._margin = window.SonarWidgets.Timeline.defaults.margin; + + // Export global variables + this.data = function (_) { + return param.call(this, '_data', _); + }; + + this.metrics = function (_) { + return param.call(this, '_metrics', _); + }; + + this.snapshots = function (_) { + return param.call(this, '_snapshots', _); + }; + + this.events = function (_) { + return param.call(this, '_events', _); + }; + + this.width = function (_) { + return param.call(this, '_width', _); + }; + + this.height = function (_) { + return param.call(this, '_height', _); + }; + + this.margin = function (_) { + return param.call(this, '_margin', _); + }; + + }; + + window.SonarWidgets.Timeline.prototype.render = function () { + var widget = this; + + this.svg = this.container.append('svg') + .attr('class', 'sonar-d3'); + + this.gWrap = this.svg.append('g'); + + this.gtimeAxis = this.gWrap.append('g') + .attr('class', 'axis x'); + + this.plotWrap = this.gWrap.append('g') + .attr('class', 'plot'); + + this.plotWrapFocus = this.gWrap.append('g') + .attr('class', 'plot'); + + this.scanner = this.plotWrap.append('line'); + + this.infoWrap = this.gWrap.append('g') + .attr('class', 'info'); + + this.infoDate = this.infoWrap.append('text') + .attr('class', 'info-text info-text-bold'); + + this.infoEvent = this.infoWrap.append('text') + .attr('class', 'info-text info-text-small'); + + this.gWrap + .attr('transform', trans(this.margin().left, this.margin().top)); + + + // Configure scales + var timeDomain = this.data() + .map(function(_) { + return d3.extent(_, function(d) { return d.x; }); + }) + .reduce(function(p, c) { + return p.concat(c); + }, d3.extent(this.events(), function(d) { return d.d; })); + + this.time = d3.time.scale().domain(d3.extent(timeDomain)); + this.timeFocus = d3.time.scale().domain(d3.extent(timeDomain)); + + this.y = this.data().map(function(_) { + return d3.scale.linear() + .domain(d3.extent(_, function(d) { return d.y; })); + }); + + this.color = d3.scale.category10(); + + + // Configure the axis + this.timeAxis = d3.svg.axis() + .scale(this.time) + .orient('bottom'); + + + // Configure lines and points + this.lines = []; + this.glines = []; + this.markers = []; + this.data().forEach(function(_, i) { + var line = d3.svg.line() + .x(function(d) { return widget.time(d.x); }) + .y(function(d) { return widget.y[i](d.y); }) + .interpolate('linear'); + + var gline = widget.plotWrap.append('path') + .attr('class', 'line') + .style('stroke', function() { return widget.color(i); }); + + widget.lines.push(line); + widget.glines.push(gline); + + var marker = widget.plotWrap.selectAll('.marker').data(_); + marker.enter().append('circle') + .attr('class', 'line-marker') + .attr('r', 3) + .style('stroke', function() { return widget.color(i); }); + marker.exit().remove(); + + widget.markers.push(marker); + }); + + + // Configure scanner + this.scanner + .attr('class', 'scanner') + .attr('y1', 0); + + + // Configure info + this.infoWrap + .attr('transform', trans(0, -10)) + .style('visibility', 'hidden'); + + this.infoDate + .attr('transform', trans(0, 0)); + + this.infoMetrics = []; + this.metrics().forEach(function(d, i) { + var infoMetric = widget.infoWrap.append('g') + .attr('class', 'metric-legend') + .attr('transform', function() { return trans(110 + i * 150, -1); }); + + infoMetric.append('text') + .attr('class', 'info-text-small') + .attr('transform', trans(10, 0)); + + infoMetric.append('circle') + .attr('class', 'metric-legend-line') + .attr('transform', trans(0, -4)) + .attr('r', 4) + .style('fill', function() { return widget.color(i); }); + + widget.infoMetrics.push(infoMetric); + }); + + this.infoEvent + .attr('transform', trans(0, 20)); + + + // Configure events + this.gevents = this.gWrap.append('g') + .attr('class', 'axis events') + .selectAll('.event-tick') + .data(this.events()); + + this.gevents.enter().append('line') + .attr('class', 'event-tick') + .attr('y2', -8); + + this.gevents.exit().remove(); + + + // Set event listeners + this.svg.on('mousemove', function() { + var mx = d3.mouse(widget.plotWrap.node())[0], + cl = closest(widget.data()[0], mx, function(d) { return widget.time(d.x); }), + sx = widget.time(widget.data()[0][cl].x); + + widget.markers.forEach(function(marker) { + marker.style('opacity', 0); + d3.select(marker[0][cl]).style('opacity', 1); + }); + + widget.scanner + .attr('x1', sx) + .attr('x2', sx); + + widget.infoDate + .text(d3.time.format('%b %d, %Y')(widget.data()[0][cl].x)); + + var metricsLines = widget.data().map(function(d, i) { + return widget.metrics()[i] + ': ' + d[cl].yl; + }); + + metricsLines.forEach(function(d, i) { + widget.infoMetrics[i].select('text').text(d); + }); + + widget.gevents.attr('y2', -8); + widget.infoEvent.text(''); + widget.events().forEach(function(d, i) { + if (d.d - widget.data()[0][cl].x === 0) { + d3.select(widget.gevents[0][i]).attr('y2', -12); + + widget.infoEvent + .text(widget.events()[i].l.map(function(d) { return d.n; }).join(', ')); + } + }); + + }); + + this.svg + .on('mouseenter', function() { + widget.scanner.style('visibility', 'visible'); + widget.infoWrap.style('visibility', 'visible'); + }) + .on('mouseleave', function() { + widget.markers.forEach(function(marker) { + marker.style('opacity', 0); + }); + + widget.scanner.style('visibility', 'hidden'); + widget.infoWrap.style('visibility', 'hidden'); + + widget.gevents.attr('y2', -8); + }); + + this.svg.on('dblclick', function() { + console.log('dbclick'); + }); + + + this.update(); + + return this; + }; + + + + window.SonarWidgets.Timeline.prototype.update = function() { + var widget = this, + width = this.container.property('offsetWidth'); + + this.width(width > 100 ? width : 100); + + + // Update svg canvas + this.svg + .attr('width', this.width()) + .attr('height', this.height()); + + + // Update available size + this.availableWidth = this.width() - this.margin().left - this.margin().right; + this.availableHeight = this.height() - this.margin().top - this.margin().bottom; + + + // Update scales + this.time + .range([0, this.availableWidth]); + + this.y.forEach(function(scale) { + scale.range([widget.availableHeight, 0]); + }); + + + // Update the axis + this.gtimeAxis.attr('transform', trans(0, this.availableHeight + this.margin().bottom - 30)); + + this.gtimeAxis.transition().call(this.timeAxis); + + + // Update lines and points + this.data().forEach(function(_, i) { + widget.glines[i] + .transition() + .attr('d', widget.lines[i](_)); + + widget.markers[i] + .data(_) + .transition() + .attr('transform', function(d) { return trans(widget.time(d.x), widget.y[i](d.y)); }); + }); + + + // Update scanner + this.scanner + .attr('y2', this.availableHeight + 10); + + + // Update events + this.gevents + .transition() + .attr('transform', function(d) { return trans(widget.time(d.d), widget.availableHeight + 10); }); + + }; + + + + window.SonarWidgets.Timeline.defaults = { + width: 350, + height: 150, + margin: { top: 30, right: 10, bottom: 40, left: 10 } + }; + + + + // Some helper functions + + // Gets or sets parameter + function param(name, value) { + if (value == null) { + return this[name]; + } else { + this[name] = value; + return this; + } + } + + // Helper for create the translate(x, y) string + function trans(left, top) { + return 'translate(' + left + ', ' + top + ')'; + } + + // Helper for find the closest number in array + function closest(array, number, getter) { + var cl = null; + array.forEach(function(value, i) { + if (cl == null || + Math.abs(getter(value) - number) < Math.abs(getter(array[cl]) - number)) { + cl = i; + } + }); + return cl; + } + +})(); diff --git a/sonar-server/src/main/webapp/stylesheets/style.css b/sonar-server/src/main/webapp/stylesheets/style.css index de82270ec34..0216eb8cb59 100644 --- a/sonar-server/src/main/webapp/stylesheets/style.css +++ b/sonar-server/src/main/webapp/stylesheets/style.css @@ -2510,3 +2510,65 @@ textarea.width100 { line-height: 16px; padding: 4px 2px; } + + + + +.sonar-d3 { + +} + +.sonar-d3 .axis path { + fill: none; + stroke: #444; +} + +.sonar-d3 .tick line { + stroke: #444; +} + +.sonar-d3 .tick text { + fill: #444; +} + +.sonar-d3 .plot .line { + fill: none; + stroke: #000; + stroke-width: 2; +} + +.sonar-d3 .plot .line-marker { + fill: #fff; + stroke: #000; + stroke-width: 2; + opacity: 0; +} + +.sonar-d3 .plot .scanner { + stroke: #000; + opacity: 0.25; + visibility: hidden; +} + +.sonar-d3 .info { + +} + +.sonar-d3 .info-text { + font-size: 13px; +} + +.sonar-d3 .info-text-bold { + font-weight: bold; +} + +.sonar-d3 .info-text-small { + font-size: 11px; +} + +.sonar-d3 .event-tick { + fill: none; + stroke: #000; + stroke-width: 1px; + transition: all 0.3s ease; +} -- cgit v1.2.3