]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6357 improve display of chart on the project overview page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 26 Oct 2015 15:23:18 +0000 (16:23 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 26 Oct 2015 15:23:18 +0000 (16:23 +0100)
14 files changed:
server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js
server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js
server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js
server/sonar-web/src/main/js/apps/overview/domain/measures-list.js
server/sonar-web/src/main/js/apps/overview/domain/timeline.js
server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js
server/sonar-web/src/main/js/components/charts/bar-chart.js
server/sonar-web/src/main/js/components/charts/bubble-chart.js
server/sonar-web/src/main/js/components/charts/line-chart.js
server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/timeline.js [deleted file]
server/sonar-web/src/main/js/components/charts/treemap.js
server/sonar-web/src/main/js/components/charts/word-cloud.js

index 403d8b69120c9d6e00a446d2df17ee7c61d6d25d..a6510d60fafd0b4a3da9095d251e88af6905552a 100644 (file)
@@ -1,5 +1,7 @@
 import React from 'react';
+
 import { getMeasures } from '../../../api/measures';
+import DrilldownLink from '../helpers/drilldown-link';
 
 
 const METRICS = [
@@ -36,25 +38,35 @@ export class CoverageDetails extends React.Component {
     });
   }
 
-  renderCoverage (coverage, lineCoverage, branchCoverage) {
+  renderValue (value, metricKey) {
+    if (value != null) {
+      return <DrilldownLink component={this.props.component.key} metric={metricKey}>
+        {window.formatMeasure(value, 'PERCENT')}
+      </DrilldownLink>;
+    } else {
+      return '—';
+    }
+  }
+
+  renderCoverage (coverage, lineCoverage, branchCoverage, prefix) {
     return <table className="data zebra">
       <tbody>
       <tr>
         <td>Coverage</td>
         <td className="thin nowrap text-right">
-          {formatCoverage(coverage)}
+          {this.renderValue(coverage, prefix + 'coverage')}
         </td>
       </tr>
       <tr>
         <td>Line Coverage</td>
         <td className="thin nowrap text-right">
-          {formatCoverage(lineCoverage)}
+          {this.renderValue(lineCoverage, prefix + 'line_coverage')}
         </td>
       </tr>
       <tr>
         <td>Branch Coverage</td>
         <td className="thin nowrap text-right">
-          {formatCoverage(branchCoverage)}
+          {this.renderValue(branchCoverage, prefix + 'branch_coverage')}
         </td>
       </tr>
       </tbody>
@@ -70,7 +82,8 @@ export class CoverageDetails extends React.Component {
       {this.renderCoverage(
           this.state.measures['coverage'],
           this.state.measures['line_coverage'],
-          this.state.measures['branch_coverage'])}
+          this.state.measures['branch_coverage'],
+          '')}
     </div>;
   }
 
@@ -83,7 +96,8 @@ export class CoverageDetails extends React.Component {
       {this.renderCoverage(
           this.state.measures['it_coverage'],
           this.state.measures['it_line_coverage'],
-          this.state.measures['it_branch_coverage'])}
+          this.state.measures['it_branch_coverage'],
+          'it_')}
     </div>;
   }
 
@@ -98,7 +112,8 @@ export class CoverageDetails extends React.Component {
       {this.renderCoverage(
           this.state.measures['overall_coverage'],
           this.state.measures['overall_line_coverage'],
-          this.state.measures['overall_branch_coverage'])}
+          this.state.measures['overall_branch_coverage'],
+          'overall_')}
     </div>;
   }
 
index 65034729d7961ee32c60450fb3a9c56ef3e49480..0bc37699561ba9dc0848c9a5b81cd8b57b2e9b1a 100644 (file)
@@ -1,6 +1,6 @@
 import React from 'react';
-import { getMeasures } from '../../../api/measures';
-import { formatMeasure } from '../formatting';
+
+import { DomainMeasuresList } from '../domain/measures-list';
 
 
 const METRICS = [
@@ -13,70 +13,11 @@ const METRICS = [
 ];
 
 
-function format (value, metric) {
-  return value != null ? formatMeasure(value, metric) : '—';
-}
-
-
 export class TestsDetails extends React.Component {
-  constructor () {
-    super();
-    this.state = { measures: {} };
-  }
-
-  componentDidMount () {
-    this.requestDetails();
-  }
-
-  requestDetails () {
-    return getMeasures(this.props.component.key, METRICS).then(measures => {
-      this.setState({ measures });
-    });
-  }
-
   render () {
     return <div className="overview-domain-section">
-      <h2 className="overview-title">Tests Details</h2>
-      <table className="data zebra">
-        <tbody>
-        <tr>
-          <td>Tests</td>
-          <td className="thin nowrap text-right">
-            {format(this.state.measures['tests'], 'tests')}
-          </td>
-        </tr>
-        <tr>
-          <td>Skipped Tests</td>
-          <td className="thin nowrap text-right">
-            {format(this.state.measures['skipped_tests'], 'skipped_tests')}
-          </td>
-        </tr>
-        <tr>
-          <td>Test Errors</td>
-          <td className="thin nowrap text-right">
-            {format(this.state.measures['test_errors'], 'test_errors')}
-          </td>
-        </tr>
-        <tr>
-          <td>Test Failures</td>
-          <td className="thin nowrap text-right">
-            {format(this.state.measures['test_failures'], 'test_failures')}
-          </td>
-        </tr>
-        <tr>
-          <td>Tests Execution Time</td>
-          <td className="thin nowrap text-right">
-            {format(this.state.measures['test_execution_time'], 'test_execution_time')}
-          </td>
-        </tr>
-        <tr>
-          <td>Tests Success</td>
-          <td className="thin nowrap text-right">
-            {format(this.state.measures['test_success_density'], 'test_success_density')}
-          </td>
-        </tr>
-        </tbody>
-      </table>
+      <h2 className="overview-title">Tests</h2>
+      <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
     </div>;
   }
 }
index 730795e11aca1fe156dbd0f888026fab58bb8a5a..6adef652fe69c9b86408e1b860f95676e7c285ec 100644 (file)
@@ -90,11 +90,9 @@ export class DomainBubbleChart extends React.Component {
         tooltip: this.getTooltip(component)
       };
     });
-    let xGrid = this.state.files.map(component => getMeasure(component, this.props.xMetric));
     let formatXTick = (tick) => window.formatMeasure(tick, this.state.xMetric.type);
     let formatYTick = (tick) => window.formatMeasure(tick, this.state.yMetric.type);
     return <BubbleChart items={items}
-                        xGrid={xGrid}
                         height={HEIGHT}
                         padding={[25, 30, 50, 60]}
                         formatXTick={formatXTick}
index 16feb0425ae91aaf3319b502413cc52882e76551..32c10b9e43e7ff5123a9436426374ada9c7a8b4e 100644 (file)
@@ -5,11 +5,6 @@ import DrilldownLink from '../helpers/drilldown-link';
 import { getMeasures } from '../../../api/measures';
 
 
-function format (value, type) {
-  return value != null ? window.formatMeasure(value, type) : '—';
-}
-
-
 export class DomainMeasuresList extends React.Component {
   constructor () {
     super();
@@ -30,15 +25,23 @@ export class DomainMeasuresList extends React.Component {
     return _.findWhere(this.props.metrics, { key: metricKey });
   }
 
+  renderValue (value, metricKey, metricType) {
+    if (value != null) {
+      return <DrilldownLink component={this.props.component.key} metric={metricKey}>
+        {window.formatMeasure(value, metricType)}
+      </DrilldownLink>;
+    } else {
+      return '—';
+    }
+  }
+
   render () {
     let rows = this.props.metricsToDisplay.map(metric => {
       let metricObject = this.getMetricObject(metric);
       return <tr key={metric}>
         <td>{metricObject.name}</td>
         <td className="thin nowrap text-right">
-          <DrilldownLink component={this.props.component.key} metric={metric}>
-            {format(this.state.measures[metric], metricObject.type)}
-          </DrilldownLink>
+          {this.renderValue(this.state.measures[metric], metric, metricObject.type)}
         </td>
       </tr>;
     });
index ff671f1fc71307287ab7cebc8148fc44b6ac88df..7fdee332a63cdeb190a67b21d89d97c5e359f9ff 100644 (file)
@@ -79,11 +79,21 @@ export class DomainTimeline extends React.Component {
     </div>;
   }
 
+  renderWhenNoHistoricalData () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      There is no historical data.
+    </div>;
+  }
+
   renderLineChart () {
     if (this.state.loading) {
       return this.renderLoading();
     }
 
+    if (!this.state.events.length || !this.state.snapshots.length) {
+      return this.renderWhenNoHistoricalData();
+    }
+
     let events = this.prepareEvents();
     let currentMetricType = _.findWhere(this.props.metrics, { key: this.state.currentMetric }).type;
 
index dace42331305607ff1f9e187b06177a25a936f6e..b1b9ec791385ee4b555c5893ad79f51c9c1290b4 100644 (file)
@@ -1,6 +1,6 @@
 import React from 'react';
-import { getMeasures } from '../../../api/measures';
-import { formatMeasure } from '../formatting';
+
+import { DomainMeasuresList } from '../domain/measures-list';
 
 
 const METRICS = [
@@ -12,52 +12,10 @@ const METRICS = [
 
 
 export class DuplicationsDetails extends React.Component {
-  constructor () {
-    super();
-    this.state = { measures: {} };
-  }
-
-  componentDidMount () {
-    this.requestDetails();
-  }
-
-  requestDetails () {
-    return getMeasures(this.props.component.key, METRICS).then(measures => {
-      this.setState({ measures });
-    });
-  }
-
   render () {
     return <div className="overview-domain-section">
-      <h2 className="overview-title">Details</h2>
-      <table className="data zebra">
-        <tbody>
-        <tr>
-          <td>Duplications</td>
-          <td className="thin nowrap text-right">
-            {formatMeasure(this.state.measures['duplicated_lines_density'], 'duplicated_lines_density')}
-          </td>
-        </tr>
-        <tr>
-          <td>Blocks</td>
-          <td className="thin nowrap text-right">
-            {formatMeasure(this.state.measures['duplicated_blocks'], 'duplicated_blocks')}
-          </td>
-        </tr>
-        <tr>
-          <td>Files</td>
-          <td className="thin nowrap text-right">
-            {formatMeasure(this.state.measures['duplicated_files'], 'duplicated_files')}
-          </td>
-        </tr>
-        <tr>
-          <td>Lines</td>
-          <td className="thin nowrap text-right">
-            {formatMeasure(this.state.measures['duplicated_lines'], 'duplicated_lines')}
-          </td>
-        </tr>
-        </tbody>
-      </table>
+      <h2 className="overview-title">Duplications</h2>
+      <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
     </div>;
   }
 }
index 347d45d96730959e163b64f858d6568e526d8498..c7444fedcb9a76f1e270e0bbe22c60f1509ccc2a 100644 (file)
@@ -1,31 +1,33 @@
 import d3 from 'd3';
 import React from 'react';
 
-export class BarChart extends React.Component {
-  constructor (props) {
-    super();
-    this.state = { width: props.width, height: props.height };
-  }
+import { ResizeMixin } from './mixins/resize-mixin';
+import { TooltipsMixin } from './mixins/tooltips-mixin';
 
-  componentDidMount () {
-    if (!this.props.width || !this.props.height) {
-      this.handleResize();
-      window.addEventListener('resize', this.handleResize.bind(this));
-    }
-  }
+export const BarChart = React.createClass({
+  mixins: [ResizeMixin, TooltipsMixin],
 
-  componentWillUnmount () {
-    if (!this.props.width || !this.props.height) {
-      window.removeEventListener('resize', this.handleResize.bind(this));
-    }
-  }
+  propTypes: {
+    data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+    xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
+    xValues: React.PropTypes.arrayOf(React.PropTypes.any),
+    height: React.PropTypes.number,
+    padding: React.PropTypes.arrayOf(React.PropTypes.number),
+    barsWidth: React.PropTypes.number
+  },
 
-  handleResize () {
-    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
-    let newWidth = this.props.width || boundingClientRect.width;
-    let newHeight = this.props.height || boundingClientRect.height;
-    this.setState({ width: newWidth, height: newHeight });
-  }
+  getDefaultProps() {
+    return {
+      xTicks: [],
+      xValues: [],
+      padding: [10, 10, 10, 10],
+      barsWidth: 40
+    };
+  },
+
+  getInitialState () {
+    return { width: this.props.width, height: this.props.height };
+  },
 
   renderXTicks (xScale, yScale) {
     if (!this.props.xTicks.length) {
@@ -38,7 +40,7 @@ export class BarChart extends React.Component {
       return <text key={index} className="bar-chart-tick" x={x} y={y} dy="1.5em">{tick}</text>;
     });
     return <g>{ticks}</g>;
-  }
+  },
 
   renderXValues (xScale, yScale) {
     if (!this.props.xValues.length) {
@@ -51,7 +53,7 @@ export class BarChart extends React.Component {
       return <text key={index} className="bar-chart-tick" x={x} y={y} dy="-1em">{value}</text>;
     });
     return <g>{ticks}</g>;
-  }
+  },
 
   renderBars (xScale, yScale) {
     let bars = this.props.data.map((d, index) => {
@@ -63,7 +65,7 @@ export class BarChart extends React.Component {
                    x={x} y={y} width={this.props.barsWidth} height={height}/>;
     });
     return <g>{bars}</g>;
-  }
+  },
 
   render () {
     if (!this.state.width || !this.state.height) {
@@ -89,19 +91,4 @@ export class BarChart extends React.Component {
       </g>
     </svg>;
   }
-}
-
-BarChart.defaultProps = {
-  xTicks: [],
-  xValues: [],
-  padding: [10, 10, 10, 10],
-  barsWidth: 40
-};
-
-BarChart.propTypes = {
-  data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-  xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
-  xValues: React.PropTypes.arrayOf(React.PropTypes.any),
-  padding: React.PropTypes.arrayOf(React.PropTypes.number),
-  barsWidth: React.PropTypes.number
-};
+});
index 82edf22ce4eb3239afef0787c2f293567078634d..87f7ef84f5a24b75baf7dac9739ae8c97b40d096 100644 (file)
@@ -1,13 +1,24 @@
-import $ from 'jquery';
 import d3 from 'd3';
 import React from 'react';
 
-export class Bubble extends React.Component {
+import { ResizeMixin } from './mixins/resize-mixin';
+import { TooltipsMixin } from './mixins/tooltips-mixin';
+
+
+export const Bubble = React.createClass({
+  propTypes: {
+    x: React.PropTypes.number.isRequired,
+    y: React.PropTypes.number.isRequired,
+    r: React.PropTypes.number.isRequired,
+    tooltip: React.PropTypes.string,
+    link: React.PropTypes.string
+  },
+
   handleClick () {
     if (this.props.link) {
       window.location = this.props.link;
     }
-  }
+  },
 
   render () {
     let tooltipAttrs = {};
@@ -17,48 +28,45 @@ export class Bubble extends React.Component {
         'title': this.props.tooltip
       };
     }
-    return <circle onClick={this.handleClick.bind(this)} className="bubble-chart-bubble"
+    return <circle onClick={this.handleClick} className="bubble-chart-bubble"
                    r={this.props.r} {...tooltipAttrs}
                    transform={`translate(${this.props.x}, ${this.props.y})`}/>;
   }
-}
-
-
-export class BubbleChart extends React.Component {
-  constructor (props) {
-    super();
-    this.state = { width: props.width, height: props.height };
-  }
-
-  componentDidMount () {
-    if (!this.props.width || !this.props.height) {
-      this.handleResize();
-      window.addEventListener('resize', this.handleResize.bind(this));
-    }
-    this.initTooltips();
-  }
-
-  componentDidUpdate () {
-    this.initTooltips();
-  }
-
-  componentWillUnmount () {
-    if (!this.props.width || !this.props.height) {
-      window.removeEventListener('resize', this.handleResize.bind(this));
-    }
-  }
-
-  handleResize () {
-    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
-    let newWidth = this.props.width || boundingClientRect.width;
-    let newHeight = this.props.height || boundingClientRect.height;
-    this.setState({ width: newWidth, height: newHeight });
-  }
-
-  initTooltips () {
-    $('[data-toggle="tooltip"]', React.findDOMNode(this))
-        .tooltip({ container: 'body', placement: 'bottom', html: true });
-  }
+});
+
+
+export const BubbleChart = React.createClass({
+  mixins: [ResizeMixin, TooltipsMixin],
+
+  propTypes: {
+    items: React.PropTypes.arrayOf(React.PropTypes.object),
+    sizeRange: React.PropTypes.arrayOf(React.PropTypes.number),
+    displayXGrid: React.PropTypes.bool,
+    displayXTicks: React.PropTypes.bool,
+    displayYGrid: React.PropTypes.bool,
+    displayYTicks: React.PropTypes.bool,
+    height: React.PropTypes.number,
+    padding: React.PropTypes.arrayOf(React.PropTypes.number),
+    formatXTick: React.PropTypes.func,
+    formatYTick: React.PropTypes.func
+  },
+
+  getDefaultProps() {
+    return {
+      sizeRange: [5, 45],
+      displayXGrid: true,
+      displayYGrid: true,
+      displayXTicks: true,
+      displayYTicks: true,
+      padding: [10, 10, 10, 10],
+      formatXTick: d => d,
+      formatYTick: d => d
+    };
+  },
+
+  getInitialState() {
+    return { width: this.props.width, height: this.props.height };
+  },
 
   getXRange (xScale, sizeScale, availableWidth) {
     var minX = d3.min(this.props.items, d => xScale(d.x) - sizeScale(d.size)),
@@ -66,7 +74,7 @@ export class BubbleChart extends React.Component {
         dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0],
         dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
     return [dMinX, availableWidth - dMaxX];
-  }
+  },
 
   getYRange (yScale, sizeScale, availableHeight) {
     var minY = d3.min(this.props.items, d => yScale(d.y) - sizeScale(d.size)),
@@ -74,7 +82,7 @@ export class BubbleChart extends React.Component {
         dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1],
         dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
     return [availableHeight - dMaxY, dMinY];
-  }
+  },
 
   renderXGrid (xScale, yScale) {
     if (!this.props.displayXGrid) {
@@ -92,7 +100,7 @@ export class BubbleChart extends React.Component {
     });
 
     return <g ref="xGrid">{lines}</g>;
-  }
+  },
 
   renderYGrid (xScale, yScale) {
     if (!this.props.displayYGrid) {
@@ -110,7 +118,7 @@ export class BubbleChart extends React.Component {
     });
 
     return <g ref="yGrid">{lines}</g>;
-  }
+  },
 
   renderXTicks (xScale, yScale) {
     if (!this.props.displayXTicks) {
@@ -127,7 +135,7 @@ export class BubbleChart extends React.Component {
     });
 
     return <g>{ticks}</g>;
-  }
+  },
 
   renderYTicks (xScale, yScale) {
     if (!this.props.displayYTicks) {
@@ -145,7 +153,7 @@ export class BubbleChart extends React.Component {
     });
 
     return <g>{ticks}</g>;
-  }
+  },
 
   render () {
     if (!this.state.width || !this.state.height) {
@@ -156,27 +164,27 @@ export class BubbleChart extends React.Component {
     let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
 
     let xScale = d3.scale.linear()
-        .domain([0, d3.max(this.props.items, d => d.x)])
-        .range([0, availableWidth])
-        .nice();
+                   .domain([0, d3.max(this.props.items, d => d.x)])
+                   .range([0, availableWidth])
+                   .nice();
     let yScale = d3.scale.linear()
-        .domain([0, d3.max(this.props.items, d => d.y)])
-        .range([availableHeight, 0])
-        .nice();
+                   .domain([0, d3.max(this.props.items, d => d.y)])
+                   .range([availableHeight, 0])
+                   .nice();
     let sizeScale = d3.scale.linear()
-        .domain([0, d3.max(this.props.items, d => d.size)])
-        .range(this.props.sizeRange);
+                      .domain([0, d3.max(this.props.items, d => d.size)])
+                      .range(this.props.sizeRange);
 
     xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
     yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
 
     let bubbles = this.props.items
-        .map((item, index) => {
-          return <Bubble key={index}
-                         tooltip={item.tooltip}
-                         link={item.link}
-                         x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>;
-        });
+                      .map((item, index) => {
+                        return <Bubble key={index}
+                                       tooltip={item.tooltip}
+                                       link={item.link}
+                                       x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>;
+                      });
 
     return <svg className="bubble-chart" width={this.state.width} height={this.state.height}>
       <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
@@ -188,25 +196,4 @@ export class BubbleChart extends React.Component {
       </g>
     </svg>;
   }
-}
-
-BubbleChart.defaultProps = {
-  sizeRange: [5, 45],
-  displayXGrid: true,
-  displayYGrid: true,
-  displayXTicks: true,
-  displayYTicks: true,
-  tooltips: [],
-  padding: [10, 10, 10, 10],
-  formatXTick: d => d,
-  formatYTick: d => d
-};
-
-BubbleChart.propTypes = {
-  sizeRange: React.PropTypes.arrayOf(React.PropTypes.number),
-  displayXGrid: React.PropTypes.bool,
-  displayYGrid: React.PropTypes.bool,
-  padding: React.PropTypes.arrayOf(React.PropTypes.number),
-  formatXTick: React.PropTypes.func,
-  formatYTick: React.PropTypes.func
-};
+});
index bbb14f3a69dcd02cccab347d940fc57d3b49477c..a92ea08cab4b0248b31c9630604208066fe1c085 100644 (file)
@@ -1,31 +1,41 @@
 import d3 from 'd3';
 import React from 'react';
 
-export class LineChart extends React.Component {
-  constructor (props) {
-    super();
-    this.state = { width: props.width, height: props.height };
-  }
-
-  componentDidMount () {
-    if (!this.props.width || !this.props.height) {
-      this.handleResize();
-      window.addEventListener('resize', this.handleResize.bind(this));
-    }
-  }
-
-  componentWillUnmount () {
-    if (!this.props.width || !this.props.height) {
-      window.removeEventListener('resize', this.handleResize.bind(this));
-    }
-  }
-
-  handleResize () {
-    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
-    let newWidth = this.props.width || boundingClientRect.width;
-    let newHeight = this.props.height || boundingClientRect.height;
-    this.setState({ width: newWidth, height: newHeight });
-  }
+import { ResizeMixin } from './mixins/resize-mixin';
+import { TooltipsMixin } from './mixins/tooltips-mixin';
+
+
+export const LineChart = React.createClass({
+  mixins: [ResizeMixin, TooltipsMixin],
+
+  propTypes: {
+    data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+    xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
+    xValues: React.PropTypes.arrayOf(React.PropTypes.any),
+    padding: React.PropTypes.arrayOf(React.PropTypes.number),
+    backdropConstraints: React.PropTypes.arrayOf(React.PropTypes.number),
+    displayBackdrop: React.PropTypes.bool,
+    displayPoints: React.PropTypes.bool,
+    displayVerticalGrid: React.PropTypes.bool,
+    height: React.PropTypes.number,
+    interpolate: React.PropTypes.string
+  },
+
+  getDefaultProps() {
+    return {
+      displayBackdrop: true,
+      displayPoints: true,
+      displayVerticalGrid: true,
+      xTicks: [],
+      xValues: [],
+      padding: [10, 10, 10, 10],
+      interpolate: 'basis'
+    };
+  },
+
+  getInitialState() {
+    return { width: this.props.width, height: this.props.height };
+  },
 
   renderBackdrop (xScale, yScale) {
     if (!this.props.displayBackdrop) {
@@ -33,10 +43,10 @@ export class LineChart extends React.Component {
     }
 
     let area = d3.svg.area()
-        .x(d => xScale(d.x))
-        .y0(yScale.range()[0])
-        .y1(d => yScale(d.y))
-        .interpolate(this.props.interpolate);
+                 .x(d => xScale(d.x))
+                 .y0(yScale.range()[0])
+                 .y1(d => yScale(d.y))
+                 .interpolate(this.props.interpolate);
 
     let data = this.props.data;
     if (this.props.backdropConstraints) {
@@ -46,7 +56,7 @@ export class LineChart extends React.Component {
 
     // TODO extract styling
     return <path d={area(data)} fill="#4b9fd5" fillOpacity="0.2"/>;
-  }
+  },
 
   renderPoints (xScale, yScale) {
     if (!this.props.displayPoints) {
@@ -58,7 +68,7 @@ export class LineChart extends React.Component {
       return <circle key={index} className="line-chart-point" r="3" cx={x} cy={y}/>;
     });
     return <g>{points}</g>;
-  }
+  },
 
   renderVerticalGrid (xScale, yScale) {
     if (!this.props.displayVerticalGrid) {
@@ -71,7 +81,7 @@ export class LineChart extends React.Component {
       return <line key={index} className="line-chart-grid" x1={x} x2={x} y1={y1} y2={y2}/>;
     });
     return <g>{lines}</g>;
-  }
+  },
 
   renderXTicks (xScale, yScale) {
     if (!this.props.xTicks.length) {
@@ -84,7 +94,7 @@ export class LineChart extends React.Component {
       return <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em">{tick}</text>;
     });
     return <g>{ticks}</g>;
-  }
+  },
 
   renderXValues (xScale, yScale) {
     if (!this.props.xValues.length) {
@@ -97,16 +107,16 @@ export class LineChart extends React.Component {
       return <text key={index} className="line-chart-tick" x={x} y={y} dy="-1em">{value}</text>;
     });
     return <g>{ticks}</g>;
-  }
+  },
 
   renderLine (xScale, yScale) {
     let path = d3.svg.line()
-        .x(d => xScale(d.x))
-        .y(d => yScale(d.y))
-        .interpolate(this.props.interpolate);
+                 .x(d => xScale(d.x))
+                 .y(d => yScale(d.y))
+                 .interpolate(this.props.interpolate);
 
     return <path className="line-chart-path" d={path(this.props.data)}/>;
-  }
+  },
 
   render () {
     if (!this.state.width || !this.state.height) {
@@ -118,11 +128,11 @@ export class LineChart extends React.Component {
 
     let maxY = d3.max(this.props.data, d => d.y);
     let xScale = d3.scale.linear()
-        .domain(d3.extent(this.props.data, d => d.x))
-        .range([0, availableWidth]);
+                   .domain(d3.extent(this.props.data, d => d.x))
+                   .range([0, availableWidth]);
     let yScale = d3.scale.linear()
-        .domain([0, maxY])
-        .range([availableHeight, 0]);
+                   .domain([0, maxY])
+                   .range([availableHeight, 0]);
 
     return <svg className="line-chart" width={this.state.width} height={this.state.height}>
       <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
@@ -135,22 +145,4 @@ export class LineChart extends React.Component {
       </g>
     </svg>;
   }
-}
-
-LineChart.defaultProps = {
-  displayBackdrop: true,
-  displayPoints: true,
-  displayVerticalGrid: true,
-  xTicks: [],
-  xValues: [],
-  padding: [10, 10, 10, 10],
-  interpolate: 'basis'
-};
-
-LineChart.propTypes = {
-  data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-  xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
-  xValues: React.PropTypes.arrayOf(React.PropTypes.any),
-  padding: React.PropTypes.arrayOf(React.PropTypes.number),
-  backdropConstraints: React.PropTypes.arrayOf(React.PropTypes.number)
-};
+});
diff --git a/server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js b/server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js
new file mode 100644 (file)
index 0000000..206cb90
--- /dev/null
@@ -0,0 +1,27 @@
+import React from 'react';
+
+export const ResizeMixin = {
+  componentDidMount () {
+    if (this.isResizable()) {
+      this.handleResize();
+      window.addEventListener('resize', this.handleResize);
+    }
+  },
+
+  componentWillUnmount () {
+    if (this.isResizable()) {
+      window.removeEventListener('resize', this.handleResize);
+    }
+  },
+
+  handleResize () {
+    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+    let newWidth = this.props.width || boundingClientRect.width;
+    let newHeight = this.props.height || boundingClientRect.height;
+    this.setState({ width: newWidth, height: newHeight });
+  },
+
+  isResizable() {
+    return !this.props.width || !this.props.height;
+  }
+};
diff --git a/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js
new file mode 100644 (file)
index 0000000..2b3fc24
--- /dev/null
@@ -0,0 +1,17 @@
+import $ from 'jquery';
+import React from 'react';
+
+export const TooltipsMixin = {
+  componentDidMount () {
+    this.initTooltips();
+  },
+
+  componentDidUpdate () {
+    this.initTooltips();
+  },
+
+  initTooltips () {
+    $('[data-toggle="tooltip"]', React.findDOMNode(this))
+        .tooltip({ container: 'body', placement: 'bottom', html: true });
+  }
+};
diff --git a/server/sonar-web/src/main/js/components/charts/timeline.js b/server/sonar-web/src/main/js/components/charts/timeline.js
deleted file mode 100644 (file)
index d200ff1..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-import d3 from 'd3';
-import React from 'react';
-
-export class Timeline extends React.Component {
-  constructor (props) {
-    super();
-    this.state = { width: props.width, height: props.height };
-  }
-
-  componentDidMount () {
-    if (!this.props.width || !this.props.height) {
-      this.handleResize();
-      window.addEventListener('resize', this.handleResize.bind(this));
-    }
-  }
-
-  componentWillUnmount () {
-    if (!this.props.width || !this.props.height) {
-      window.removeEventListener('resize', this.handleResize.bind(this));
-    }
-  }
-
-  handleResize () {
-    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
-    let newWidth = this.props.width || boundingClientRect.width;
-    let newHeight = this.props.height || boundingClientRect.height;
-    this.setState({ width: newWidth, height: newHeight });
-  }
-
-  renderBackdrop (xScale, yScale, maxY) {
-    if (!this.props.displayBackdrop) {
-      return null;
-    }
-
-    let area = d3.svg.area()
-        .x(d => xScale(d.date))
-        .y0(maxY)
-        .y1(d => yScale(d.value))
-        .interpolate(this.props.interpolate);
-
-    // TODO extract styling
-    return <path d={area(this.props.snapshots)} fill="#4b9fd5" fillOpacity="0.2"/>;
-  }
-
-  renderLine (xScale, yScale) {
-    let path = d3.svg.line()
-        .x(d => xScale(d.date))
-        .y(d => yScale(d.value))
-        .interpolate(this.props.interpolate);
-
-    // TODO extract styling
-    return <path d={path(this.props.snapshots)} stroke="#4b9fd5" strokeWidth={this.props.lineWidth} fill="none"/>;
-  }
-
-  render () {
-    if (!this.state.width || !this.state.height) {
-      return <div/>;
-    }
-
-    let maxY = d3.max(this.props.snapshots, d => d.value);
-    let xScale = d3.time.scale()
-        .domain(d3.extent(this.props.snapshots, d => d.date))
-        .range([0, this.state.width - this.props.lineWidth]);
-    let yScale = d3.scale.linear()
-        .domain([0, maxY])
-        .range([this.state.height, 0]);
-
-    return <svg width={this.state.width} height={this.state.height}>
-      <g transform={`translate(${this.props.lineWidth / 2}, ${this.props.lineWidth / 2})`}>
-        {this.renderBackdrop(xScale, yScale, maxY)}
-        {this.renderLine(xScale, yScale)}
-      </g>
-    </svg>;
-  }
-}
-
-Timeline.defaultProps = {
-  lineWidth: 2,
-  displayBackdrop: true,
-  interpolate: 'basis'
-};
-
-Timeline.propTypes = {
-  snapshots: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
-};
index d00ce0be0fce49a20c427e4b6a2e78a3605c4c7d..51b140e003d797d529de24ab377c96d0ba640417 100644 (file)
@@ -1,13 +1,15 @@
-import $ from 'jquery';
 import _ from 'underscore';
 import d3 from 'd3';
 import React from 'react';
 
+import { ResizeMixin } from './mixins/resize-mixin';
+import { TooltipsMixin } from './mixins/tooltips-mixin';
+
 
 const SIZE_SCALE = d3.scale.linear()
-    .domain([3, 15])
-    .range([11, 18])
-    .clamp(true);
+                     .domain([3, 15])
+                     .range([11, 18])
+                     .clamp(true);
 
 
 function mostCommitPrefix (strings) {
@@ -25,7 +27,17 @@ function mostCommitPrefix (strings) {
 }
 
 
-export class TreemapRect extends React.Component {
+export const TreemapRect = React.createClass({
+  propTypes: {
+    x: React.PropTypes.number.isRequired,
+    y: React.PropTypes.number.isRequired,
+    width: React.PropTypes.number.isRequired,
+    height: React.PropTypes.number.isRequired,
+    fill: React.PropTypes.string.isRequired,
+    label: React.PropTypes.string.isRequired,
+    prefix: React.PropTypes.string
+  },
+
   render () {
     let tooltipAttrs = {};
     if (this.props.tooltip) {
@@ -48,52 +60,20 @@ export class TreemapRect extends React.Component {
            style={{ maxWidth: this.props.width }}/>
     </div>;
   }
-}
+});
 
-TreemapRect.propTypes = {
-  x: React.PropTypes.number.isRequired,
-  y: React.PropTypes.number.isRequired,
-  width: React.PropTypes.number.isRequired,
-  height: React.PropTypes.number.isRequired,
-  fill: React.PropTypes.string.isRequired
-};
 
+export const Treemap = React.createClass({
+  mixins: [ResizeMixin, TooltipsMixin],
 
-export class Treemap extends React.Component {
-  constructor (props) {
-    super();
-    this.state = { width: props.width, height: props.height };
-  }
+  propTypes: {
+    items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+    height: React.PropTypes.number
+  },
 
-  componentDidMount () {
-    if (!this.props.width || !this.props.height) {
-      this.handleResize();
-      window.addEventListener('resize', this.handleResize.bind(this));
-    }
-    this.initTooltips();
-  }
-
-  componentDidUpdate () {
-    this.initTooltips();
-  }
-
-  componentWillUnmount () {
-    if (!this.props.width || !this.props.height) {
-      window.removeEventListener('resize', this.handleResize.bind(this));
-    }
-  }
-
-  initTooltips () {
-    $('[data-toggle="tooltip"]', React.findDOMNode(this))
-        .tooltip({ container: 'body', placement: 'top', html: true });
-  }
-
-  handleResize () {
-    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
-    let newWidth = this.props.width || boundingClientRect.width;
-    let newHeight = this.props.height || boundingClientRect.height;
-    this.setState({ width: newWidth, height: newHeight });
-  }
+  getInitialState() {
+    return { width: this.props.width, height: this.props.height };
+  },
 
   render () {
     if (!this.state.width || !this.state.height || !this.props.items.length) {
@@ -101,12 +81,12 @@ export class Treemap extends React.Component {
     }
 
     let sizeScale = d3.scale.linear()
-        .domain([0, d3.max(this.props.items, d => d.size)])
-        .range([5, 45]);
+                      .domain([0, d3.max(this.props.items, d => d.size)])
+                      .range([5, 45]);
     let treemap = d3.layout.treemap()
-        .round(true)
-        .value(d => sizeScale(d.size))
-        .size([this.state.width, 360]);
+                    .round(true)
+                    .value(d => sizeScale(d.size))
+                    .size([this.state.width, 360]);
     let nodes = treemap
         .nodes({ children: this.props.items })
         .filter(d => !d.children);
@@ -133,8 +113,4 @@ export class Treemap extends React.Component {
       </div>
     </div>;
   }
-}
-
-Treemap.propTypes = {
-  items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
-};
+});
index e2382d9e0350f34688099c5f8685f86b48534d54..53c36ee76aff101239f6f282331dd74be8d929dd 100644 (file)
@@ -1,8 +1,16 @@
-import $ from 'jquery';
 import _ from 'underscore';
 import React from 'react';
 
-export class Word extends React.Component {
+import { TooltipsMixin } from './mixins/tooltips-mixin';
+
+export const Word = React.createClass({
+  propTypes: {
+    size: React.PropTypes.number.isRequired,
+    text: React.PropTypes.string.isRequired,
+    tooltip: React.PropTypes.string,
+    link: React.PropTypes.string.isRequired
+  },
+
   render () {
     let tooltipAttrs = {};
     if (this.props.tooltip) {
@@ -13,22 +21,22 @@ export class Word extends React.Component {
     }
     return <a {...tooltipAttrs} style={{ fontSize: this.props.size }} href={this.props.link}>{this.props.text}</a>;
   }
-}
+});
 
 
-export class WordCloud extends React.Component {
-  componentDidMount () {
-    this.initTooltips();
-  }
+export const WordCloud = React.createClass({
+  mixins: [TooltipsMixin],
 
-  componentDidUpdate () {
-    this.initTooltips();
-  }
+  propTypes: {
+    items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+    sizeRange: React.PropTypes.arrayOf(React.PropTypes.number)
+  },
 
-  initTooltips () {
-    $('[data-toggle="tooltip"]', React.findDOMNode(this))
-        .tooltip({ container: 'body', placement: 'bottom', html: true });
-  }
+  getDefaultProps() {
+    return {
+      sizeRange: [10, 24]
+    };
+  },
 
   render () {
     let len = this.props.items.length;
@@ -38,8 +46,8 @@ export class WordCloud extends React.Component {
     });
 
     let sizeScale = d3.scale.linear()
-        .domain([0, d3.max(this.props.items, d => d.size)])
-        .range(this.props.sizeRange);
+                      .domain([0, d3.max(this.props.items, d => d.size)])
+                      .range(this.props.sizeRange);
     let words = sortedItems
         .map((item, index) => <Word key={index}
                                     text={item.text}
@@ -48,12 +56,4 @@ export class WordCloud extends React.Component {
                                     tooltip={item.tooltip}/>);
     return <div className="word-cloud">{words}</div>;
   }
-}
-
-WordCloud.defaultProps = {
-  sizeRange: [10, 24]
-};
-
-WordCloud.propTypes = {
-  sizeRange: React.PropTypes.arrayOf(React.PropTypes.number)
-};
+});