diff options
Diffstat (limited to 'server/sonar-web/src/main/js/libs/widgets/bubble-chart.js')
-rw-r--r-- | server/sonar-web/src/main/js/libs/widgets/bubble-chart.js | 541 |
1 files changed, 541 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/libs/widgets/bubble-chart.js b/server/sonar-web/src/main/js/libs/widgets/bubble-chart.js new file mode 100644 index 00000000000..fc166404423 --- /dev/null +++ b/server/sonar-web/src/main/js/libs/widgets/bubble-chart.js @@ -0,0 +1,541 @@ +/* + * 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. + */ +/*global d3:false, baseUrl:false */ + +window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets; + +(function () { + + window.SonarWidgets.BubbleChart = function () { + // Set default values + this._components = []; + this._metrics = []; + this._metricsPriority = []; + 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; + this._options = {}; + + // Export global variables + this.metrics = function (_) { + return param.call(this, '_metrics', _); + }; + + this.metricsPriority = function (_) { + return param.call(this, '_metricsPriority', _); + }; + + this.components = function (_) { + return param.call(this, '_components', _); + }; + + 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', _); + }; + + this.options = function (_) { + return param.call(this, '_options', _); + }; + }; + + + window.SonarWidgets.BubbleChart.prototype.hasValidData = function () { + var widget = this, + noInvalidEntry = true, + atLeastOneValueOnX = false, + atLeastOneValueOnY = false; + this.components().forEach(function(component) { + noInvalidEntry = noInvalidEntry && + !!component.measures[widget.metricsPriority()[0]] && + !!component.measures[widget.metricsPriority()[1]]; + atLeastOneValueOnX = atLeastOneValueOnX || + (component.measures[widget.metricsPriority()[0]] || {}).fval !== '-'; + atLeastOneValueOnY = atLeastOneValueOnY || + (component.measures[widget.metricsPriority()[1]] || {}).fval !== '-'; + }); + return !!noInvalidEntry && !!atLeastOneValueOnX && !!atLeastOneValueOnY; + }; + + + window.SonarWidgets.BubbleChart.prototype.init = function (container) { + this.width(container.property('offsetWidth')); + + this.svg = container.append('svg') + .attr('class', 'sonar-d3'); + 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.gWrap + .attr('transform', trans(this.margin().left, this.margin().top)); + }; + + + window.SonarWidgets.BubbleChart.prototype.initMetrics = function () { + var widget = this; + + this.xMetric = this.metricsPriority()[0]; + this.getXMetric = function(d) { + return d.measures[widget.xMetric].val; + }; + + this.yMetric = this.metricsPriority()[1]; + this.getYMetric = function(d) { + return d.measures[widget.yMetric].val; + }; + + this.sizeMetric = this.metricsPriority()[2]; + this.getSizeMetric = function(d) { + return !!d.measures[widget.sizeMetric] ? d.measures[widget.sizeMetric].val : 0; + }; + }; + + + window.SonarWidgets.BubbleChart.prototype.initScales = function () { + var widget = this; + this + .xLog(this.options().xLog) + .yLog(this.options().yLog); + + 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.range([0, this.availableWidth]); + this.y.range([this.availableHeight, 0]); + this.size.range([5, 45]); + + if (this.components().length > 1) { + this.x.domain(d3.extent(this.components(), function (d) { + return widget.getXMetric(d); + })); + this.y.domain(d3.extent(this.components(), function (d) { + return widget.getYMetric(d); + })); + this.size.domain(d3.extent(this.components(), function (d) { + return widget.getSizeMetric(d); + })); + } else { + var singleComponent = this.components()[0], + xm = this.getXMetric(singleComponent), + ym = this.getYMetric(singleComponent), + sm = this.getSizeMetric(singleComponent); + this.x.domain([xm * 0.8, xm * 1.2]); + this.y.domain([ym * 0.8, ym * 1.2]); + this.size.domain([sm * 0.8, sm * 1.2]); + } + }; + + + window.SonarWidgets.BubbleChart.prototype.initBubbles = function () { + var widget = this; + + // Create bubbles + this.items = this.plotWrap.selectAll('.item') + .data(this.components()); + + + // 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(widget.getSizeMetric(d)); + }) + .style('fill', function () { + return widget.bubbleColor(); + }) + .style('fill-opacity', 0.2) + .style('stroke', function () { + return widget.bubbleColor(); + }) + .style('transition', 'all 0.2s ease') + + .attr('title', function (d) { + var xMetricName = widget.metrics()[widget.xMetric].name, + yMetricName = widget.metrics()[widget.yMetric].name, + sizeMetricName = widget.metrics()[widget.sizeMetric].name, + + xMetricValue = d.measures[widget.xMetric].fval, + yMetricValue = d.measures[widget.yMetric].fval, + sizeMetricValue = d.measures[widget.sizeMetric].fval; + + return '<div class="text-left">' + + collapsedDirFromPath(d.longName) + '<br>' + + fileFromPath(d.longName) + '<br>' + '<br>' + + xMetricName + ': ' + xMetricValue + '<br>' + + yMetricName + ': ' + yMetricValue + '<br>' + + sizeMetricName + ': ' + sizeMetricValue + + '</div>'; + }) + .attr('data-placement', 'bottom') + .attr('data-toggle', 'tooltip'); + + this.items.exit().remove(); + + this.items.sort(function (a, b) { + return widget.getSizeMetric(b) - widget.getSizeMetric(a); + }); + }; + + + window.SonarWidgets.BubbleChart.prototype.initBubbleEvents = function () { + var widget = this; + this.items + .on('click', function (d) { + window.location = widget.options().baseUrl + '?id=' + encodeURIComponent(d.key); + }) + .on('mouseenter', function () { + d3.select(this).select('circle') + .style('fill-opacity', 0.8); + }) + .on('mouseleave', function () { + d3.select(this).select('circle') + .style('fill-opacity', 0.2); + }); + }; + + + window.SonarWidgets.BubbleChart.prototype.initAxes = function () { + // X + this.xAxis = d3.svg.axis() + .scale(this.x) + .orient('bottom'); + + this.gxAxisLabel = this.gxAxis.append('text') + .text(this.metrics()[this.xMetric].name) + .style('font-weight', 'bold') + .style('text-anchor', 'middle'); + + + // Y + this.yAxis = d3.svg.axis() + .scale(this.y) + .orient('left'); + + this.gyAxis.attr('transform', trans(60 - this.margin().left, 0)); + + this.gyAxisLabel = this.gyAxis.append('text') + .text(this.metrics()[this.yMetric].name) + .style('font-weight', 'bold') + .style('text-anchor', 'middle'); + }; + + + window.SonarWidgets.BubbleChart.prototype.initGrid = function () { + this.gxGridLines = this.gxGrid.selectAll('line').data(this.x.ticks()).enter() + .append('line'); + + this.gyGridLines = this.gyGrid.selectAll('line').data(this.y.ticks()).enter() + .append('line'); + + this.gGrid.selectAll('line') + .style('stroke', '#000') + .style('stroke-opacity', 0.25); + }; + + + window.SonarWidgets.BubbleChart.prototype.render = function (container) { + var containerS = container; + + container = d3.select(container); + + if (!this.hasValidData()) { + container.text(this.options().noMainMetric); + return; + } + + this.init(container); + this.initMetrics(); + this.initScales(); + this.initBubbles(); + this.initBubbleEvents(); + this.initAxes(); + this.initGrid(); + this.update(containerS); + + jQuery('[data-toggle="tooltip"]').tooltip({ container: 'body', html: true }); + + return this; + }; + + + window.SonarWidgets.BubbleChart.prototype.adjustScalesAfterUpdate = function () { + var widget = this; + // X + var minX = d3.min(this.components(), function (d) { + return widget.x(widget.getXMetric(d)) - widget.size(widget.getSizeMetric(d)); + }), + maxX = d3.max(this.components(), function (d) { + return widget.x(widget.getXMetric(d)) + widget.size(widget.getSizeMetric(d)); + }), + dMinX = minX < 0 ? this.x.range()[0] - minX : this.x.range()[0], + dMaxX = maxX > this.x.range()[1] ? maxX - this.x.range()[1] : 0; + this.x.range([dMinX, this.availableWidth - dMaxX]); + + // Y + var minY = d3.min(this.components(), function (d) { + return widget.y(widget.getYMetric(d)) - widget.size(widget.getSizeMetric(d)); + }), + maxY = d3.max(this.components(), function (d) { + return widget.y(widget.getYMetric(d)) + widget.size(widget.getSizeMetric(d)); + }), + dMinY = minY < 0 ? this.y.range()[1] - minY: this.y.range()[1], + dMaxY = maxY > this.y.range()[0] ? maxY - this.y.range()[0] : 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(); + }; + + + window.SonarWidgets.BubbleChart.prototype.updateScales = function () { + var widget = this; + this.x.range([0, this.availableWidth]); + this.y.range([this.availableHeight, 0]); + + if (this.components().length > 1) { + this.x.domain(d3.extent(this.components(), function (d) { + return widget.getXMetric(d); + })); + this.y.domain(d3.extent(this.components(), function (d) { + return widget.getYMetric(d); + })); + } else { + var singleComponent = this.components()[0], + xm = this.getXMetric(singleComponent), + ym = this.getYMetric(singleComponent), + sm = this.getSizeMetric(singleComponent); + this.x.domain([xm * 0.8, xm * 1.2]); + this.y.domain([ym * 0.8, ym * 1.2]); + this.size.domain([sm * 0.8, sm * 1.2]); + } + + if (this.x.domain()[0] === 0 && this.x.domain()[1] === 0) { + this.x.domain([0, 1]); + } + if (this.y.domain()[0] === 0 && this.y.domain()[1] === 0) { + this.y.domain([0, 1]); + } + + // 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); + } + }; + + + window.SonarWidgets.BubbleChart.prototype.updateBubbles = function () { + var widget = this; + this.items + .transition() + .attr('transform', function (d) { + return trans(widget.x(widget.getXMetric(d)), widget.y(widget.getYMetric(d))); + }); + }; + + + window.SonarWidgets.BubbleChart.prototype.updateAxes = function () { + // 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)'); + }; + + + window.SonarWidgets.BubbleChart.prototype.updateGrid = function () { + var widget = this; + 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.prototype.update = function (container) { + container = d3.select(container); + + var 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; + + this.updateScales(); + this.adjustScalesAfterUpdate(); + this.updateBubbles(); + this.updateAxes(); + this.updateGrid(); + }; + + + + window.SonarWidgets.BubbleChart.defaults = { + width: 350, + height: 150, + margin: { top: 10, 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 + ')'; + } + +})(); |