aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/libs/widgets/bubble-chart.js
diff options
context:
space:
mode:
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.js541
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 + ')';
+ }
+
+})();