<% else %>
<% timeline = 'timeline' + widget.id.to_s %>
- <div id="timeline-chart-<%= widget.id -%>" style="height: <%= chartHeight %>px"> </div>
- <script type="text/javascript+protovis">
+ <div id="timeline-chart-<%= widget.id -%>"></div>
+ <script>
function d(y, m, d, h, min, s) {
return new Date(y, m, d, h, min, s);
}
var data = <%= js_data -%>;
var snapshots = <%= js_snapshots -%>;
var metrics = <%= js_metrics -%>;
- var events = <%= js_events ? js_events : "null" -%>;
+ var events = <%= js_events ? js_events : "[]" -%>;
var <%= timeline -%> = new SonarWidgets.Timeline('timeline-chart-<%= widget.id -%>')
.height(<%= chartHeight -%>)
.data(data)
<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>
<%= 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' %>
+++ /dev/null
-/*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 + ')';
- }
-
-})();
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();
-};
--- /dev/null
+/*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 + ')';
+ }
+
+})();
--- /dev/null
+//******************* 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;
+ }
+
+})();
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;
+}