diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2013-10-27 13:48:40 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2013-10-28 10:08:49 +0100 |
commit | 2ae51a4c28f5ccfb85540b7f52007ea1f0843ccf (patch) | |
tree | bfb396fbab5b74100d6471f13acae3aa3a4efe24 /sonar-server | |
parent | d7671af5aef43a82ca275687148f7ac50f0d7294 (diff) | |
download | sonarqube-2ae51a4c28f5ccfb85540b7f52007ea1f0843ccf.tar.gz sonarqube-2ae51a4c28f5ccfb85540b7f52007ea1f0843ccf.zip |
SONAR-4817 Replace timeline code base
Diffstat (limited to 'sonar-server')
-rw-r--r-- | sonar-server/pom.xml | 3 | ||||
-rw-r--r-- | sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb | 3 | ||||
-rw-r--r-- | sonar-server/src/main/webapp/javascripts/protovis-sonar.js | 241 | ||||
-rw-r--r-- | sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js (renamed from sonar-server/src/main/webapp/javascripts/bubble-chart.js) | 8 | ||||
-rw-r--r-- | sonar-server/src/main/webapp/javascripts/widgets/timeline.js | 399 | ||||
-rw-r--r-- | sonar-server/src/main/webapp/stylesheets/style.css | 62 |
6 files changed, 469 insertions, 247 deletions
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 @@ <include>**/jquery-ui.min.js</include> <include>**/third-party/d3.v3.min.js</include> <include>**/select2.min.js</include> - <include>**/bubble-chart.js</include> + <include>**/widgets/bubble-chart.js</include> + <include>**/widgets/timeline.js</include> <include>**/application-min.js</include> <include>**/dashboard-min.js</include> <include>**/duplication-min.js</include> 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/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: - * - <code> - 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(); - </code> - * - */ - -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/bubble-chart.js b/sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js index 9f31d662f02..d35366559c3 100644 --- a/sonar-server/src/main/webapp/javascripts/bubble-chart.js +++ b/sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js @@ -75,7 +75,7 @@ window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets; this.plotWrap = this.gWrap.append('g'); this.infoWrap = this.gWrap.append('g'); - this.infoName = this.infoWrap.append('text'); + this.infoDate = this.infoWrap.append('text'); this.infoMetrics = this.infoWrap.append('text'); this.gWrap @@ -152,7 +152,7 @@ window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets; d3.select(this).select('circle') .style('fill-opacity', 0.8); - widget.infoName.text(d.longName); + widget.infoDate.text(d.longName); widget.infoMetrics.text( widget.metrics().x + ': ' + d.xMetricFormatted + '; ' + widget.metrics().y + ': ' + d.yMetricFormatted + '; ' + @@ -162,7 +162,7 @@ window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets; d3.select(this).select('circle') .style('fill-opacity', 0.2); - widget.infoName.text(''); + widget.infoDate.text(''); widget.infoMetrics.text(''); }); @@ -208,7 +208,7 @@ window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets; this.infoWrap .attr('transform', trans(-this.margin().left, -this.margin().top + 20)); - this.infoName + this.infoDate .style('text-anchor', 'start') .style('font-weight', 'bold'); 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: + * + <code> + 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(); + </code> + * + */ + + +/*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; +} |