aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/libs/widgets/stack-area.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/libs/widgets/stack-area.js')
-rw-r--r--server/sonar-web/src/main/js/libs/widgets/stack-area.js404
1 files changed, 404 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/libs/widgets/stack-area.js b/server/sonar-web/src/main/js/libs/widgets/stack-area.js
new file mode 100644
index 00000000000..993828be5bf
--- /dev/null
+++ b/server/sonar-web/src/main/js/libs/widgets/stack-area.js
@@ -0,0 +1,404 @@
+/*
+ * 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.
+ */
+window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets;
+
+(function () {
+
+ window.SonarWidgets.StackArea = 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._colors = [];
+ this._width = window.SonarWidgets.StackArea.defaults.width;
+ this._height = window.SonarWidgets.StackArea.defaults.height;
+ this._margin = window.SonarWidgets.StackArea.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.colors = function (_) {
+ return param.call(this, '_colors', _);
+ };
+
+ 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.StackArea.prototype.initScales = function() {
+ var widget = this,
+ colorsLength = widget.colors().length;
+ var timeDomain = this.data()
+ .map(function(_) {
+ return d3.extent(_, function(d) {
+ return d.x;
+ });
+ })
+ .reduce(function(p, c) {
+ return p.concat(c);
+ }, []);
+
+ this.time = d3.time.scale().domain(d3.extent(timeDomain));
+
+ this.y = d3.scale.linear()
+ .domain([0, d3.max(this.stackDataTop, function(d) {
+ return d.y0 + d.y;
+ })])
+ .nice();
+
+ this.color = function(i) {
+ return widget.colors()[i % colorsLength][0];
+ };
+ };
+
+
+ window.SonarWidgets.StackArea.prototype.initAxis = function() {
+ this.timeAxis = d3.svg.axis()
+ .scale(this.time)
+ .orient('bottom')
+ .ticks(5);
+ };
+
+
+ window.SonarWidgets.StackArea.prototype.initArea = function() {
+ var widget = this;
+ this.area = d3.svg.area()
+ .x(function(d) { return widget.time(d.x); })
+ .y0(function(d) { return widget.y(d.y0); })
+ .y1(function(d) { return widget.y(d.y0 + d.y); });
+
+ this.areaLine = d3.svg.line()
+ .x(function(d) { return widget.time(d.x); })
+ .y(function(d) { return widget.y(d.y0 + d.y); });
+ };
+
+
+ window.SonarWidgets.StackArea.prototype.initInfo = function() {
+ var widget = this;
+ this.infoWrap
+ .attr('class', 'info')
+ .attr('transform', trans(0, -60));
+
+ this.infoDate
+ .attr('class', 'info-text info-text-bold')
+ .attr('transform', trans(0, 0));
+
+ this.infoTotal
+ .attr('class', 'info-text info-text-small')
+ .attr('transform', trans(0, 18));
+
+ this.infoSnapshot
+ .attr('class', 'info-text info-text-small')
+ .attr('transform', trans(0, 54));
+
+ this.infoMetrics = [];
+ var prevX = 120;
+ this.metrics().forEach(function(d, i) {
+ var infoMetric = widget.infoWrap.append('g');
+
+ var infoMetricText = infoMetric.append('text')
+ .attr('class', 'info-text-small')
+ .attr('transform', trans(10, 0))
+ .text(widget.metrics()[i]);
+
+ infoMetric.append('circle')
+ .attr('transform', trans(0, -4))
+ .attr('r', 4)
+ .style('fill', function() { return widget.color(i); });
+
+ // Align metric labels
+ infoMetric
+ .attr('transform', function() {
+ return trans(prevX, -1 + (i % 3) * 18);
+ });
+
+ widget.infoMetrics.push(infoMetric);
+
+ if (i % 3 === 2) {
+ prevX += (infoMetricText.node().getComputedTextLength() + 70);
+ }
+ });
+ };
+
+
+ window.SonarWidgets.StackArea.prototype.initEvents = function() {
+ var widget = this;
+ this.events = widget.snapshots()
+ .filter(function(d) { return d.e.length > 0; });
+
+ 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.selectSnapshot = function(cl) {
+ var dataX = widget.data()[0][cl].x,
+ sx = widget.time(dataX),
+ snapshotIndex = null,
+ eventIndex = null;
+
+ // Update scanner position
+ widget.scanner
+ .attr('x1', sx)
+ .attr('x2', sx);
+
+
+ // Update metric labels
+ var metricsLines = widget.data().map(function(d, i) {
+ var value = d[cl].fy || d[cl].y;
+ return widget.metrics()[i] + ': ' + value;
+ });
+
+ metricsLines.forEach(function(d, i) {
+ widget.infoMetrics[i].select('text').text(d);
+ });
+
+
+ // Update snapshot info
+ this.snapshots().forEach(function(d, i) {
+ if (d.d - dataX === 0) {
+ snapshotIndex = i;
+ }
+ });
+
+ if (snapshotIndex != null) {
+ widget.infoSnapshot
+ .text(this.snapshots()[snapshotIndex].e.join(', '));
+ }
+
+
+ // Update info
+ widget.infoDate
+ .text(moment(widget.data()[0][cl].x).format('LL'));
+
+ var snapshotValue = this.snapshots()[snapshotIndex].fy,
+ totalValue = snapshotValue || (widget.stackDataTop[cl].y0 + widget.stackDataTop[cl].y);
+ widget.infoTotal
+ .text('Total: ' + totalValue);
+
+
+ // Update event
+ this.events.forEach(function(d, i) {
+ if (d.d - dataX === 0) {
+ eventIndex = i;
+ }
+ });
+
+ widget.gevents.attr('y2', -8);
+ d3.select(widget.gevents[0][eventIndex]).attr('y2', -12);
+ };
+
+
+ // 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); });
+ widget.selectSnapshot(cl);
+ });
+ };
+
+
+ window.SonarWidgets.StackArea.prototype.render = function () {
+ 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.scanner = this.plotWrap.append('line');
+
+ this.infoWrap = this.gWrap.append('g');
+ this.infoDate = this.infoWrap.append('text');
+ this.infoSnapshot = this.infoWrap.append('text');
+ this.infoTotal = this.infoWrap.append('text');
+
+ this.gWrap
+ .attr('transform', trans(this.margin().left, this.margin().top));
+
+ // Configure stack
+ this.stack = d3.layout.stack();
+ this.stackData = this.stack(this.data());
+ this.stackDataTop = this.stackData[this.stackData.length - 1];
+
+ this.initScales();
+ this.initAxis();
+ this.initArea();
+
+ // Configure scanner
+ this.scanner
+ .attr('class', 'scanner')
+ .attr('y1', 0);
+
+ this.initInfo();
+ this.initEvents();
+ this.update();
+
+ return this;
+ };
+
+
+ window.SonarWidgets.StackArea.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.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 area
+ this.garea = this.plotWrap.selectAll('.area')
+ .data(this.stackData)
+ .enter()
+ .insert('path', ':first-child')
+ .attr('class', 'area')
+ .attr('d', function(d) { return widget.area(d); })
+ .style('fill', function(d, i) { return widget.color(i); });
+
+ this.gareaLine = this.plotWrap.selectAll('.area-line')
+ .data(this.stackData)
+ .enter()
+ .insert('path')
+ .attr('class', 'area-line')
+ .attr('d', function(d) { return widget.areaLine(d); })
+ .style('fill', 'none')
+ .style('stroke', '#808080');
+
+
+ // 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);
+ });
+
+
+ // Select latest values if this it the first update
+ if (!this.firstUpdate) {
+ this.selectSnapshot(widget.data()[0].length - 1);
+
+ this.firstUpdate = true;
+ }
+
+ };
+
+
+
+ window.SonarWidgets.StackArea.defaults = {
+ width: 350,
+ height: 150,
+ margin: { top: 80, right: 10, bottom: 40, left: 40 }
+ };
+
+
+
+ // 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;
+ }
+
+})();