]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4817 Replace timeline code base
authorStas Vilchik <vilchiks@gmail.com>
Sun, 27 Oct 2013 12:48:40 +0000 (13:48 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 28 Oct 2013 09:08:49 +0000 (10:08 +0100)
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/timeline.html.erb
sonar-server/pom.xml
sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb
sonar-server/src/main/webapp/javascripts/bubble-chart.js [deleted file]
sonar-server/src/main/webapp/javascripts/protovis-sonar.js
sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js [new file with mode: 0644]
sonar-server/src/main/webapp/javascripts/widgets/timeline.js [new file with mode: 0644]
sonar-server/src/main/webapp/stylesheets/style.css

index 82b6e765e1f5721bf487e5f7ca350b4f868ab887..ea222abad370a1b31039f744e27e79ae12b56082 100644 (file)
 <%   else %>
   <% timeline = 'timeline' + widget.id.to_s %>
 
-  <div id="timeline-chart-<%= widget.id -%>" style="height: <%= chartHeight %>px">&nbsp;</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)
index 3cac776afd94df5bf2bcf60d176f7f6c15caa073..01bec19ba879be6ba750bad093dff9a689f262be 100644 (file)
                 <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>
index 367e0afaf752b9415d23c96642bbf5529644e288..18726222b8a294b141506f012d2795529c6600f1 100644 (file)
@@ -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/bubble-chart.js b/sonar-server/src/main/webapp/javascripts/bubble-chart.js
deleted file mode 100644 (file)
index 9f31d66..0000000
+++ /dev/null
@@ -1,421 +0,0 @@
-/*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 + ')';
-  }
-
-})();
index f8a79167ff1d1c2f95e2375ad77481cd8962d880..a90a2e605287b7d40919b2001498e76c8a9fba04 100644 (file)
@@ -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/widgets/bubble-chart.js b/sonar-server/src/main/webapp/javascripts/widgets/bubble-chart.js
new file mode 100644 (file)
index 0000000..d353665
--- /dev/null
@@ -0,0 +1,421 @@
+/*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 + ')';
+  }
+
+})();
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 (file)
index 0000000..16052f9
--- /dev/null
@@ -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;
+  }
+
+})();
index de82270ec3463b9a3f8a0fe15bf774fe1d0b0f0e..0216eb8cb595526bbd7d4fc3f316f69e62386736 100644 (file)
@@ -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;
+}