]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6357 add detailed "Size" panel for the "Overview" main page
authorStas Vilchik <vilchiks@gmail.com>
Fri, 23 Oct 2015 14:09:18 +0000 (16:09 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 26 Oct 2015 10:45:00 +0000 (11:45 +0100)
34 files changed:
server/sonar-web/src/main/js/api/languages.js [new file with mode: 0644]
server/sonar-web/src/main/js/api/measures.js
server/sonar-web/src/main/js/api/metrics.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/coverage/bubble-chart.js
server/sonar-web/src/main/js/apps/overview/coverage/main.js
server/sonar-web/src/main/js/apps/overview/coverage/timeline.js
server/sonar-web/src/main/js/apps/overview/coverage/treemap.js
server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/domain/measures-list.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/domain/timeline.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/domain/treemap.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js
server/sonar-web/src/main/js/apps/overview/duplications/main.js
server/sonar-web/src/main/js/apps/overview/duplications/timeline.js
server/sonar-web/src/main/js/apps/overview/duplications/treemap.js
server/sonar-web/src/main/js/apps/overview/formatting.js
server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js
server/sonar-web/src/main/js/apps/overview/helpers/metrics.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/issues/bubble-chart.js
server/sonar-web/src/main/js/apps/overview/issues/timeline.js
server/sonar-web/src/main/js/apps/overview/issues/treemap.js
server/sonar-web/src/main/js/apps/overview/main.js
server/sonar-web/src/main/js/apps/overview/size/comments-details.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/size/complexity-details.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/size/language-distribution.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/size/main.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/size/size-details.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/size/timeline.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/size/treemap.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/bar-chart.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/bubble-chart.js
server/sonar-web/src/main/js/components/charts/treemap.js
server/sonar-web/src/main/less/pages/overview.less

diff --git a/server/sonar-web/src/main/js/api/languages.js b/server/sonar-web/src/main/js/api/languages.js
new file mode 100644 (file)
index 0000000..49b3295
--- /dev/null
@@ -0,0 +1,6 @@
+import { getJSON } from '../helpers/request.js';
+
+export function getLanguages () {
+  let url = baseUrl + '/api/languages/list';
+  return getJSON(url).then(r => r.languages);
+}
index b79b56a5ef2d42b5fa3a70e836a464777ecebe86..e416e3a8882854f16957f1e6a8a7039c06206fb4 100644 (file)
@@ -7,7 +7,7 @@ export function getMeasures (componentKey, metrics) {
     let msr = r[0].msr || [];
     let measures = {};
     msr.forEach(measure => {
-      measures[measure.key] = measure.val;
+      measures[measure.key] = measure.val || measure.data;
     });
     return measures;
   });
diff --git a/server/sonar-web/src/main/js/api/metrics.js b/server/sonar-web/src/main/js/api/metrics.js
new file mode 100644 (file)
index 0000000..66bf748
--- /dev/null
@@ -0,0 +1,8 @@
+import _ from 'underscore';
+import { getJSON } from '../helpers/request.js';
+
+export function getMetrics () {
+  let url = baseUrl + '/api/metrics/search';
+  let data = { ps: 9999 };
+  return getJSON(url, data).then(r => r.metrics);
+}
index fde275f7b6c6d602e32a125cf4da46e77c881df9..826aca983641332768bd2c8aad0442b26cf47122 100644 (file)
-import _ from 'underscore';
 import React from 'react';
-import { BubbleChart } from '../../../components/charts/bubble-chart';
-import { getProjectUrl } from '../../../helpers/Url';
-import { getFiles } from '../../../api/components';
-import { formatMeasure } from '../formatting';
-
-
-const X_METRIC = 'complexity';
-const Y_METRIC = 'coverage';
-const SIZE_METRIC = 'sqale_index';
-const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC];
-const HEIGHT = 360;
-
-
-function formatInt (d) {
-  return window.formatMeasure(d, 'SHORT_INT');
-}
-
-function formatPercent (d) {
-  return window.formatMeasure(d, 'PERCENT');
-}
-
-function getMeasure (component, metric) {
-  return component.measures[metric] || 0;
-}
+import { DomainBubbleChart } from '../domain/bubble-chart';
 
 
 export class CoverageBubbleChart extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, files: [] };
-  }
-
-  componentDidMount () {
-    this.requestFiles();
-  }
-
-  requestFiles () {
-    return getFiles(this.props.component.key, COMPONENTS_METRICS).then(r => {
-      let files = r.map(file => {
-        let measures = {};
-        (file.msr || []).forEach(measure => {
-          measures[measure.key] = measure.val;
-        });
-        return _.extend(file, { measures });
-      });
-      this.setState({ loading: false, files });
-    });
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderBubbleChart () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let items = this.state.files.map(component => {
-      return {
-        x: getMeasure(component, X_METRIC),
-        y: getMeasure(component, Y_METRIC),
-        size: getMeasure(component, SIZE_METRIC),
-        link: getProjectUrl(component.key)
-      };
-    });
-    let xGrid = this.state.files.map(component => component.measures[X_METRIC]);
-    let tooltips = this.state.files.map(component => {
-      let inner = [
-        component.name,
-        `Complexity: ${formatMeasure(getMeasure(component, X_METRIC), X_METRIC)}`,
-        `Coverage: ${formatMeasure(getMeasure(component, Y_METRIC), Y_METRIC)}`,
-        `Technical Debt: ${formatMeasure(getMeasure(component, SIZE_METRIC), SIZE_METRIC)}`
-      ].join('<br>');
-      return `<div class="text-left">${inner}</div>`;
-    });
-    return <BubbleChart items={items}
-                        xGrid={xGrid}
-                        tooltips={tooltips}
-                        height={HEIGHT}
-                        padding={[25, 30, 50, 60]}
-                        formatXTick={formatInt}
-                        formatYTick={formatPercent}/>;
-  }
-
   render () {
-    return <div className="overview-bubble-chart overview-domain-dark">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project Files</h2>
-        <ul className="list-inline small">
-          <li>X: Complexity</li>
-          <li>Y: Coverage</li>
-          <li>Size: Technical Debt</li>
-        </ul>
-      </div>
-      <div>
-        {this.renderBubbleChart()}
-      </div>
-    </div>;
+    return <DomainBubbleChart {...this.props}
+        xMetric="complexity"
+        yMetric="coverage"
+        sizeMetrics={['sqale_index']}/>;
   }
 }
index 88533e138c7246bc826c08fbb61b92fb26bae3ba..084a7f94ea0736677ed8f24a8b1280cb31a4095c 100644 (file)
@@ -6,10 +6,8 @@ import { CoverageBubbleChart } from './bubble-chart';
 import { CoverageTimeline } from './timeline';
 import { CoverageTreemap } from './treemap';
 
-import { getSeverities, getTags, getAssignees } from '../../../api/issues';
 
-
-export default class CoverageDomain extends React.Component {
+export default class extends React.Component {
   render () {
     return <div className="overview-domain">
 
index 58e9a9d98e91f1f6989a8e358012e6517c010d50..60e14a00b6badace8622f94a75d861357e5d077b 100644 (file)
-import _ from 'underscore';
-import moment from 'moment';
 import React from 'react';
 
-import { LineChart } from '../../../components/charts/line-chart';
-import { formatMeasure } from '../formatting';
-import { getTimeMachineData } from '../../../api/time-machine';
-import { getEvents } from '../../../api/events';
+import { DomainTimeline } from '../domain/timeline';
+import { filterMetricsForDomains } from '../helpers/metrics';
 
 
-const COVERAGE_METRICS = [
-  'coverage',
-  'line_coverage',
-  'branch_coverage',
-  'lines_to_cover',
-  'conditions_to_cover',
-  'uncovered_lines',
-  'uncovered_conditions',
-
-  'it_coverage',
-  'it_line_coverage',
-  'it_branch_coverage',
-  'it_lines_to_cover',
-  'it_conditions_to_cover',
-  'it_uncovered_lines',
-  'it_uncovered_conditions',
-
-  'overall_coverage',
-  'overall_line_coverage',
-  'overall_branch_coverage',
-  'overall_lines_to_cover',
-  'overall_conditions_to_cover',
-  'overall_uncovered_lines',
-  'overall_uncovered_conditions'
-];
-
-const TESTS_METRICS = [
-  'tests',
-  'skipped_tests',
-  'test_errors',
-  'test_failures',
-  'test_execution_time',
-  'test_success_density'
+const DOMAINS = [
+  'Tests',
+  'Tests (Integration)',
+  'Tests (Overall)'
 ];
 
-const HEIGHT = 280;
-
 
 export class CoverageTimeline extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, currentMetric: COVERAGE_METRICS[0] };
-  }
-
-  componentDidMount () {
-    Promise.all([
-      this.requestTimeMachineData(),
-      this.requestEvents()
-    ]).then(() => this.setState({ loading: false }));
-  }
-
-  requestTimeMachineData () {
-    return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
-      let snapshots = r[0].cells.map(cell => {
-        return { date: moment(cell.d).toDate(), value: cell.v[0] };
-      });
-      this.setState({ snapshots });
-    });
-  }
-
-  requestEvents () {
-    return getEvents(this.props.component.key, 'Version').then(r => {
-      let events = r.map(event => {
-        return { version: event.n, date: moment(event.dt).toDate() };
-      });
-      events = _.sortBy(events, 'date');
-      this.setState({ events });
-    });
-  }
-
-  prepareEvents () {
-    let events = this.state.events;
-    let snapshots = this.state.snapshots;
-    return events
-        .map(event => {
-          let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
-          event.value = snapshot && snapshot.value;
-          return event;
-        })
-        .filter(event => event.value != null);
-  }
-
-  handleMetricChange () {
-    let metric = React.findDOMNode(this.refs.metricSelect).value;
-    this.setState({ currentMetric: metric }, this.requestTimeMachineData);
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderLineChart () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let events = this.prepareEvents();
-
-    let data = events.map((event, index) => {
-      return { x: index, y: event.value };
-    });
-
-    let xTicks = events.map(event => event.version.substr(0, 6));
-
-    let xValues = events.map(event => formatMeasure(event.value, this.state.currentMetric));
-
-    // TODO use leak period
-    let backdropConstraints = [
-      this.state.events.length - 2,
-      this.state.events.length - 1
-    ];
-
-    return <LineChart data={data}
-                      xTicks={xTicks}
-                      xValues={xValues}
-                      backdropConstraints={backdropConstraints}
-                      height={HEIGHT}
-                      interpolate="linear"
-                      padding={[25, 30, 50, 30]}/>;
-  }
-
-  renderTimelineMetricSelect () {
-    if (this.state.loading) {
-      return null;
-    }
-
-    let issueOptions = COVERAGE_METRICS
-        .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
-    let debtOptions = TESTS_METRICS
-        .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
-
-    return <select ref="metricSelect"
-                   className="overview-timeline-select"
-                   onChange={this.handleMetricChange.bind(this)}
-                   value={this.state.currentMetric}>
-      <optgroup label="Coverage">{issueOptions}</optgroup>
-      <optgroup label="Tests">{debtOptions}</optgroup>
-    </select>;
-  }
-
   render () {
-    return <div className="overview-timeline overview-domain-dark">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project History</h2>
-        {this.renderTimelineMetricSelect()}
-      </div>
-      <div>
-        {this.renderLineChart()}
-      </div>
-    </div>;
+    return <DomainTimeline {...this.props}
+        initialMetric="coverage"
+        metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
   }
 }
index ee4af18e8327267c260e7f0d79aa49cba9ae00c9..251d5f0751e6c56220611a2ee343b31a9465da86 100644 (file)
@@ -1,89 +1,20 @@
-import _ from 'underscore';
 import d3 from 'd3';
 import React from 'react';
 
-import { Treemap } from '../../../components/charts/treemap';
-import { formatMeasure } from '../formatting';
-import { getChildren } from '../../../api/components';
+import { DomainTreemap } from '../domain/treemap';
 
-const COMPONENTS_METRICS = [
-  'lines',
-  'coverage'
-];
-
-const HEIGHT = 360;
 
 const COLORS_5 = ['#ee0000', '#f77700', '#ffee00', '#80cc00', '#00aa00'];
 
-export class CoverageTreemap extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, components: [] };
-  }
-
-  componentDidMount () {
-    this.requestComponents();
-  }
-
-  requestComponents () {
-    return getChildren(this.props.component.key, COMPONENTS_METRICS).then(r => {
-      let components = r.map(component => {
-        let measures = {};
-        (component.msr || []).forEach(measure => {
-          measures[measure.key] = measure.val;
-        });
-        return _.extend(component, { measures });
-      });
-      this.setState({ loading: false, components });
-    });
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderTreemap () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let colorScale = d3.scale.linear().domain([0, 25, 50, 75, 100]);
-    colorScale.range(COLORS_5);
-
-    let items = this.state.components.map(component => {
-      let coverage = component.measures['coverage'];
-      console.log(coverage);
-      return {
-        size: component.measures['lines'],
-        color: coverage != null ? colorScale(coverage) : '#777'
-      };
-    });
-    let labels = this.state.components.map(component => component.name);
-    let tooltips = this.state.components.map(component => {
-      let inner = [
-        component.name,
-        `Lines: ${formatMeasure(component.measures['lines'], 'lines')}`,
-        `Coverage: ${formatMeasure(component.measures['coverage'], 'coverage')}`
-      ].join('<br>');
-      return `<div class="text-left">${inner}</div>`;
-    });
-    return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
-  }
 
+export class CoverageTreemap extends React.Component {
   render () {
-    return <div className="overview-domain-section overview-treemap">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project Components</h2>
-        <ul className="list-inline small">
-          <li>Size: Lines</li>
-          <li>Color: Coverage</li>
-        </ul>
-      </div>
-      <div>
-        {this.renderTreemap()}
-      </div>
-    </div>;
+    let scale = d3.scale.linear()
+        .domain([0, 25, 50, 75, 100])
+        .range(COLORS_5);
+    return <DomainTreemap {...this.props}
+        sizeMetric="ncloc"
+        colorMetric="coverage"
+        scale={scale}/>;
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js
new file mode 100644 (file)
index 0000000..730795e
--- /dev/null
@@ -0,0 +1,125 @@
+import _ from 'underscore';
+import React from 'react';
+import { BubbleChart } from '../../../components/charts/bubble-chart';
+import { getProjectUrl } from '../../../helpers/Url';
+import { getFiles } from '../../../api/components';
+
+
+const HEIGHT = 360;
+
+
+function getMeasure (component, metric) {
+  return component.measures[metric] || 0;
+}
+
+
+export class DomainBubbleChart extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      loading: true,
+      files: [],
+      xMetric: this.getMetricObject(props.metrics, props.xMetric),
+      yMetric: this.getMetricObject(props.metrics, props.yMetric),
+      sizeMetrics: props.sizeMetrics.map(this.getMetricObject.bind(null, props.metrics))
+    };
+  }
+
+  componentDidMount () {
+    this.requestFiles();
+  }
+
+  requestFiles () {
+    let metrics = [].concat(this.props.xMetric, this.props.yMetric, this.props.sizeMetrics);
+    return getFiles(this.props.component.key, metrics).then(r => {
+      let files = r.map(file => {
+        let measures = {};
+        (file.msr || []).forEach(measure => {
+          measures[measure.key] = measure.val;
+        });
+        return _.extend(file, { measures });
+      });
+      this.setState({ loading: false, files });
+    });
+  }
+
+  getMetricObject (metrics, metricKey) {
+    return _.findWhere(metrics, { key: metricKey });
+  }
+
+  getSizeMetricsValue (component) {
+    return this.props.sizeMetrics.reduce((previousValue, currentValue) => {
+      return previousValue + getMeasure(component, currentValue);
+    }, 0);
+  }
+
+  getSizeMetricsTitle () {
+    return this.state.sizeMetrics.map(metric => metric.name).join(' & ');
+  }
+
+  getTooltip (component) {
+    let sizeMetricsTitle = this.getSizeMetricsTitle();
+    let sizeMetricsType = this.state.sizeMetrics[0].type;
+
+    let inner = [
+      component.name,
+      `${this.state.xMetric.name}: ${window.formatMeasure(getMeasure(component, this.props.xMetric), this.state.xMetric.type)}`,
+      `${this.state.yMetric.name}: ${window.formatMeasure(getMeasure(component, this.props.yMetric), this.state.yMetric.type)}`,
+      `${sizeMetricsTitle}: ${window.formatMeasure(this.getSizeMetricsValue(component), sizeMetricsType)}`
+    ].join('<br>');
+    return `<div class="text-left">${inner}</div>`;
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderBubbleChart () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let items = this.state.files.map(component => {
+      return {
+        x: getMeasure(component, this.props.xMetric),
+        y: getMeasure(component, this.props.yMetric),
+        size: this.getSizeMetricsValue(component),
+        link: getProjectUrl(component.key),
+        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}
+                        formatYTick={formatYTick}/>;
+  }
+
+  render () {
+    return <div className="overview-bubble-chart overview-domain-dark">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project Files</h2>
+        <ul className="list-inline small">
+          <li>X: {this.state.xMetric.name}</li>
+          <li>Y: {this.state.yMetric.name}</li>
+          <li>Size: {this.getSizeMetricsTitle()}</li>
+        </ul>
+      </div>
+      <div>
+        {this.renderBubbleChart()}
+      </div>
+    </div>;
+  }
+}
+
+DomainBubbleChart.propTypes = {
+  xMetric: React.PropTypes.string.isRequired,
+  yMetric: React.PropTypes.string.isRequired,
+  sizeMetrics: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
+};
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js b/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js
new file mode 100644 (file)
index 0000000..16feb04
--- /dev/null
@@ -0,0 +1,53 @@
+import _ from 'underscore';
+import React from 'react';
+
+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();
+    this.state = { measures: {} };
+  }
+
+  componentDidMount () {
+    this.requestDetails();
+  }
+
+  requestDetails () {
+    return getMeasures(this.props.component.key, this.props.metricsToDisplay).then(measures => {
+      this.setState({ measures });
+    });
+  }
+
+  getMetricObject (metricKey) {
+    return _.findWhere(this.props.metrics, { key: metricKey });
+  }
+
+  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>
+        </td>
+      </tr>;
+    });
+    return <table className="data zebra">
+      <tbody>{rows}</tbody>
+    </table>;
+  }
+}
+
+DomainMeasuresList.propTypes = {
+  metricsToDisplay: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
+};
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/timeline.js b/server/sonar-web/src/main/js/apps/overview/domain/timeline.js
new file mode 100644 (file)
index 0000000..ff671f1
--- /dev/null
@@ -0,0 +1,153 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { LineChart } from '../../../components/charts/line-chart';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+
+
+const HEIGHT = 280;
+
+
+export class DomainTimeline extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = { loading: true, currentMetric: props.initialMetric };
+  }
+
+  componentDidMount () {
+    Promise.all([
+      this.requestTimeMachineData(),
+      this.requestEvents()
+    ]).then(() => this.setState({ loading: false }));
+  }
+
+  requestTimeMachineData () {
+    return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
+      let snapshots = r[0].cells.map(cell => {
+        return { date: moment(cell.d).toDate(), value: cell.v[0] };
+      });
+      this.setState({ snapshots });
+    });
+  }
+
+  requestEvents () {
+    return getEvents(this.props.component.key, 'Version').then(r => {
+      let events = r.map(event => {
+        return { version: event.n, date: moment(event.dt).toDate() };
+      });
+      events = _.sortBy(events, 'date');
+      this.setState({ events });
+    });
+  }
+
+  prepareEvents () {
+    let events = this.state.events;
+    let snapshots = this.state.snapshots;
+    return events
+        .map(event => {
+          let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
+          event.value = snapshot && snapshot.value;
+          return event;
+        })
+        .filter(event => event.value != null);
+  }
+
+  handleMetricChange () {
+    let metric = React.findDOMNode(this.refs.metricSelect).value;
+    this.setState({ currentMetric: metric }, this.requestTimeMachineData);
+  }
+
+  groupMetricsByDomain () {
+    return _.sortBy(
+        _.map(
+            _.groupBy(this.props.metrics, 'domain'),
+            (metricList, domain) => {
+              return {
+                domain: domain,
+                metrics: _.sortBy(metricList, 'name')
+              };
+            }),
+        'domain'
+    );
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderLineChart () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let events = this.prepareEvents();
+    let currentMetricType = _.findWhere(this.props.metrics, { key: this.state.currentMetric }).type;
+
+    let data = events.map((event, index) => {
+      return { x: index, y: event.value };
+    });
+
+    let xTicks = events.map(event => event.version.substr(0, 6));
+
+    let xValues = events.map(event => window.formatMeasure(event.value, currentMetricType));
+
+    // TODO use leak period
+    let backdropConstraints = [
+      this.state.events.length - 2,
+      this.state.events.length - 1
+    ];
+
+    return <LineChart data={data}
+                      xTicks={xTicks}
+                      xValues={xValues}
+                      backdropConstraints={backdropConstraints}
+                      height={HEIGHT}
+                      interpolate="linear"
+                      padding={[25, 30, 50, 30]}/>;
+  }
+
+  renderMetricOption (metric) {
+    return <option key={metric.key} value={metric.key}>{metric.name}</option>;
+  }
+
+  renderTimelineMetricSelect () {
+    if (this.state.loading) {
+      return null;
+    }
+    let groupedMetrics = this.groupMetricsByDomain();
+    let inner;
+    if (groupedMetrics.length > 1) {
+      inner = groupedMetrics.map(metricGroup => {
+        let options = metricGroup.metrics.map(this.renderMetricOption);
+        return <optgroup key={metricGroup.domain} label={metricGroup.domain}>{options}</optgroup>;
+      });
+    } else {
+      inner = groupedMetrics[0].metrics.map(this.renderMetricOption);
+    }
+    return <select ref="metricSelect"
+                   className="overview-timeline-select"
+                   onChange={this.handleMetricChange.bind(this)}
+                   value={this.state.currentMetric}>{inner}</select>;
+  }
+
+  render () {
+    return <div className="overview-timeline overview-domain-dark">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project History</h2>
+        {this.renderTimelineMetricSelect()}
+      </div>
+      <div>
+        {this.renderLineChart()}
+      </div>
+    </div>;
+  }
+}
+
+DomainTimeline.propTypes = {
+  metrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+  initialMetric: React.PropTypes.string.isRequired
+};
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/treemap.js b/server/sonar-web/src/main/js/apps/overview/domain/treemap.js
new file mode 100644 (file)
index 0000000..1592b58
--- /dev/null
@@ -0,0 +1,100 @@
+import _ from 'underscore';
+import React from 'react';
+
+import { Treemap } from '../../../components/charts/treemap';
+import { getChildren } from '../../../api/components';
+
+const HEIGHT = 360;
+
+
+export class DomainTreemap extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      loading: true,
+      files: [],
+      sizeMetric: this.getMetricObject(props.metrics, props.sizeMetric),
+      colorMetric: props.colorMetric ? this.getMetricObject(props.metrics, props.colorMetric) : null
+    };
+  }
+
+  componentDidMount () {
+    this.requestComponents();
+  }
+
+  requestComponents () {
+    let metrics = [this.props.sizeMetric, this.props.colorMetric];
+    return getChildren(this.props.component.key, metrics).then(r => {
+      let components = r.map(component => {
+        let measures = {};
+        (component.msr || []).forEach(measure => {
+          measures[measure.key] = measure.val;
+        });
+        return _.extend(component, { measures });
+      });
+      this.setState({ loading: false, components });
+    });
+  }
+
+  getMetricObject (metrics, metricKey) {
+    return _.findWhere(metrics, { key: metricKey });
+  }
+
+  getTooltip (component) {
+    let inner = [
+      component.name,
+      `${this.state.sizeMetric.name}: ${window.formatMeasure(component.measures[this.props.sizeMetric], this.state.sizeMetric.type)}`
+    ];
+    if (this.state.colorMetric) {
+      inner.push(`${this.state.colorMetric.name}: ${window.formatMeasure(component.measures[this.props.colorMetric], this.state.colorMetric.type)}`);
+    }
+    inner = inner.join('<br>');
+    return `<div class="text-left">${inner}</div>`;
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderTreemap () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    // TODO filter out zero sized components
+    let items = this.state.components.map(component => {
+      let colorMeasure = this.props.colorMetric ? component.measures[this.props.colorMetric] : null;
+      return {
+        size: component.measures[this.props.sizeMetric],
+        color: colorMeasure != null ? this.props.scale(colorMeasure) : '#777',
+        tooltip: this.getTooltip(component),
+        label: component.name
+      };
+    });
+    return <Treemap items={items} height={HEIGHT}/>;
+  }
+
+  render () {
+    let color = this.props.colorMetric ? <li>Color: {this.state.colorMetric.name}</li> : null;
+    return <div className="overview-domain-section overview-treemap">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project Components</h2>
+        <ul className="list-inline small">
+          <li>Size: {this.state.sizeMetric.name}</li>
+          {color}
+        </ul>
+      </div>
+      <div>
+        {this.renderTreemap()}
+      </div>
+    </div>;
+  }
+}
+
+DomainTreemap.propTypes = {
+  sizeMetric: React.PropTypes.string.isRequired,
+  colorMetric: React.PropTypes.string,
+  scale: React.PropTypes.func
+};
index 4612cdf7bb41562efdd366b5884b9273c6a7641d..299a47978f187703d02426cde49f0343681e4fb8 100644 (file)
-import _ from 'underscore';
 import React from 'react';
-import { BubbleChart } from '../../../components/charts/bubble-chart';
-import { getProjectUrl } from '../../../helpers/Url';
-import { getFiles } from '../../../api/components';
-import { formatMeasure } from '../formatting';
-
-
-const X_METRIC = 'ncloc';
-const Y_METRIC = 'duplicated_blocks';
-const SIZE_METRIC = 'duplicated_lines';
-const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC];
-const HEIGHT = 360;
-
-
-function formatInt (d) {
-  return window.formatMeasure(d, 'SHORT_INT');
-}
-
-function getMeasure (component, metric) {
-  return component.measures[metric] || 0;
-}
+import { DomainBubbleChart } from '../domain/bubble-chart';
 
 
 export class DuplicationsBubbleChart extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, files: [] };
-  }
-
-  componentDidMount () {
-    this.requestFiles();
-  }
-
-  requestFiles () {
-    return getFiles(this.props.component.key, COMPONENTS_METRICS).then(r => {
-      let files = r.map(file => {
-        let measures = {};
-        (file.msr || []).forEach(measure => {
-          measures[measure.key] = measure.val;
-        });
-        return _.extend(file, { measures });
-      });
-      this.setState({ loading: false, files });
-    });
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderBubbleChart () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let items = this.state.files.map(component => {
-      return {
-        x: getMeasure(component, X_METRIC),
-        y: getMeasure(component, Y_METRIC),
-        size: getMeasure(component, SIZE_METRIC),
-        link: getProjectUrl(component.key)
-      };
-    });
-    let xGrid = this.state.files.map(component => component.measures[X_METRIC]);
-    let tooltips = this.state.files.map(component => {
-      let inner = [
-        component.name,
-        `Lines of Code: ${formatMeasure(getMeasure(component, X_METRIC), X_METRIC)}`,
-        `Duplicated Blocks: ${formatMeasure(getMeasure(component, Y_METRIC), Y_METRIC)}`,
-        `Duplicated Lines: ${formatMeasure(getMeasure(component, SIZE_METRIC), SIZE_METRIC)}`
-      ].join('<br>');
-      return `<div class="text-left">${inner}</div>`;
-    });
-    return <BubbleChart items={items}
-                        xGrid={xGrid}
-                        tooltips={tooltips}
-                        height={HEIGHT}
-                        padding={[25, 30, 50, 60]}
-                        formatXTick={formatInt}
-                        formatYTick={formatInt}/>;
-  }
-
   render () {
-    return <div className="overview-bubble-chart overview-domain-dark">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project Files</h2>
-        <ul className="list-inline small">
-          <li>X: Lines of Code</li>
-          <li>Y: Duplicated Blocks</li>
-          <li>Size: Duplicated Lines</li>
-        </ul>
-      </div>
-      <div>
-        {this.renderBubbleChart()}
-      </div>
-    </div>;
+    return <DomainBubbleChart {...this.props}
+        xMetric="ncloc"
+        yMetric="duplicated_blocks"
+        sizeMetrics={['duplicated_lines']}/>;
   }
 }
index 3cda843b5cb0615be7e7c6cdf2b68b8196fc7627..a029f6ebb328b0c3be9f1c11bb8e5cd5e003ab27 100644 (file)
@@ -5,10 +5,8 @@ import { DuplicationsBubbleChart } from './bubble-chart';
 import { DuplicationsTimeline } from './timeline';
 import { DuplicationsTreemap } from './treemap';
 
-import { getSeverities, getTags, getAssignees } from '../../../api/issues';
 
-
-export default class DuplicationsDomain extends React.Component {
+export default class extends React.Component {
   render () {
     return <div className="overview-domain">
       <DuplicationsTimeline {...this.props}/>
index 81c8d63d8bc99c14161b6459f8d91d503e2642e6..abd6987af7dbddf1714f0ab0a4eaaa7f7867492f 100644 (file)
-import _ from 'underscore';
-import moment from 'moment';
 import React from 'react';
 
-import { LineChart } from '../../../components/charts/line-chart';
-import { formatMeasure } from '../formatting';
-import { getTimeMachineData } from '../../../api/time-machine';
-import { getEvents } from '../../../api/events';
+import { DomainTimeline } from '../domain/timeline';
+import { filterMetricsForDomains } from '../helpers/metrics';
 
 
-const DUPLICATIONS_METRICS = [
-  'duplicated_blocks',
-  'duplicated_files',
-  'duplicated_lines',
-  'duplicated_lines_density'
-];
-
-const HEIGHT = 280;
+const DOMAINS = ['Duplication'];
 
 
 export class DuplicationsTimeline extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, currentMetric: DUPLICATIONS_METRICS[0] };
-  }
-
-  componentDidMount () {
-    Promise.all([
-      this.requestTimeMachineData(),
-      this.requestEvents()
-    ]).then(() => this.setState({ loading: false }));
-  }
-
-  requestTimeMachineData () {
-    return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
-      let snapshots = r[0].cells.map(cell => {
-        return { date: moment(cell.d).toDate(), value: cell.v[0] };
-      });
-      this.setState({ snapshots });
-    });
-  }
-
-  requestEvents () {
-    return getEvents(this.props.component.key, 'Version').then(r => {
-      let events = r.map(event => {
-        return { version: event.n, date: moment(event.dt).toDate() };
-      });
-      events = _.sortBy(events, 'date');
-      this.setState({ events });
-    });
-  }
-
-  prepareEvents () {
-    let events = this.state.events;
-    let snapshots = this.state.snapshots;
-    return events
-        .map(event => {
-          let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
-          event.value = snapshot && snapshot.value;
-          return event;
-        })
-        .filter(event => event.value != null);
-  }
-
-  handleMetricChange () {
-    let metric = React.findDOMNode(this.refs.metricSelect).value;
-    this.setState({ currentMetric: metric }, this.requestTimeMachineData);
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderLineChart () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let events = this.prepareEvents();
-
-    let data = events.map((event, index) => {
-      return { x: index, y: event.value };
-    });
-
-    let xTicks = events.map(event => event.version.substr(0, 6));
-
-    let xValues = events.map(event => formatMeasure(event.value, this.state.currentMetric));
-
-    // TODO use leak period
-    let backdropConstraints = [
-      this.state.events.length - 2,
-      this.state.events.length - 1
-    ];
-
-    return <LineChart data={data}
-                      xTicks={xTicks}
-                      xValues={xValues}
-                      backdropConstraints={backdropConstraints}
-                      height={HEIGHT}
-                      interpolate="linear"
-                      padding={[25, 30, 50, 30]}/>;
-  }
-
-  renderTimelineMetricSelect () {
-    if (this.state.loading) {
-      return null;
-    }
-
-    let options = DUPLICATIONS_METRICS
-        .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
-
-    return <select ref="metricSelect"
-                   className="overview-timeline-select"
-                   onChange={this.handleMetricChange.bind(this)}
-                   value={this.state.currentMetric}>{options}</select>;
-  }
-
   render () {
-    return <div className="overview-timeline overview-domain-dark">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project History</h2>
-        {this.renderTimelineMetricSelect()}
-      </div>
-      <div>
-        {this.renderLineChart()}
-      </div>
-    </div>;
+    return <DomainTimeline {...this.props}
+        initialMetric="duplicated_lines_density"
+        metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
   }
 }
index 0b3b00e17d6f1a991c811691b44c7d28f09e82bc..019e78e4cab5a747512c92106d89b3ee41d4c31b 100644 (file)
@@ -1,88 +1,20 @@
-import _ from 'underscore';
 import d3 from 'd3';
 import React from 'react';
 
-import { Treemap } from '../../../components/charts/treemap';
-import { formatMeasure } from '../formatting';
-import { getChildren } from '../../../api/components';
+import { DomainTreemap } from '../domain/treemap';
 
-const COMPONENTS_METRICS = [
-  'ncloc',
-  'duplicated_lines_density'
-];
-
-const HEIGHT = 360;
 
 const COLORS_5 = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
 
-export class DuplicationsTreemap extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, components: [] };
-  }
-
-  componentDidMount () {
-    this.requestComponents();
-  }
-
-  requestComponents () {
-    return getChildren(this.props.component.key, COMPONENTS_METRICS).then(r => {
-      let components = r.map(component => {
-        let measures = {};
-        (component.msr || []).forEach(measure => {
-          measures[measure.key] = measure.val;
-        });
-        return _.extend(component, { measures });
-      });
-      this.setState({ loading: false, components });
-    });
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderTreemap () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let colorScale = d3.scale.linear().domain([0, 25, 50, 75, 100]);
-    colorScale.range(COLORS_5);
-
-    let items = this.state.components.map(component => {
-      let duplications = component.measures['duplicated_lines_density'];
-      return {
-        size: component.measures['ncloc'],
-        color: duplications != null ? colorScale(duplications) : '#777'
-      };
-    });
-    let labels = this.state.components.map(component => component.name);
-    let tooltips = this.state.components.map(component => {
-      let inner = [
-        component.name,
-        `Lines of Code: ${formatMeasure(component.measures['ncloc'], 'ncloc')}`,
-        `Duplications: ${formatMeasure(component.measures['duplicated_lines_density'], 'duplicated_lines_density')}`
-      ].join('<br>');
-      return `<div class="text-left">${inner}</div>`;
-    });
-    return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
-  }
 
+export class DuplicationsTreemap extends React.Component {
   render () {
-    return <div className="overview-domain-section overview-treemap">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project Components</h2>
-        <ul className="list-inline small">
-          <li>Size: Lines of Code</li>
-          <li>Color: Duplications</li>
-        </ul>
-      </div>
-      <div>
-        {this.renderTreemap()}
-      </div>
-    </div>;
+    let scale = d3.scale.linear()
+        .domain([0, 25, 50, 75, 100])
+        .range(COLORS_5);
+    return <DomainTreemap {...this.props}
+        sizeMetric="ncloc"
+        colorMetric="duplicated_lines_density"
+        scale={scale}/>;
   }
 }
index c35de0f91fd72b190d0d45a9eac5292a06b38c72..7f05531226c2ec37c76c730a7b0cd543f31fa55b 100644 (file)
@@ -12,7 +12,6 @@ const METRIC_TYPES = {
   'sqale_index': 'SHORT_WORK_DUR',
   'sqale_debt_ratio': 'PERCENT',
   'sqale_rating': 'RATING',
-  'lines': 'SHORT_INT',
 
   'coverage': 'PERCENT',
   'line_coverage': 'PERCENT',
@@ -50,7 +49,27 @@ const METRIC_TYPES = {
   'duplicated_lines': 'INT',
   'duplicated_lines_density': 'PERCENT',
 
-  'complexity': 'INT'
+  'ncloc': 'SHORT_INT',
+  'classes': 'SHORT_INT',
+  'lines': 'SHORT_INT',
+  'generated_ncloc': 'SHORT_INT',
+  'generated_lines': 'SHORT_INT',
+  'directories': 'SHORT_INT',
+  'files': 'SHORT_INT',
+  'functions': 'SHORT_INT',
+  'statements': 'SHORT_INT',
+  'public_api': 'SHORT_INT',
+
+  'complexity': 'SHORT_INT',
+  'class_complexity': 'SHORT_INT',
+  'file_complexity': 'SHORT_INT',
+  'function_complexity': 'SHORT_INT',
+
+  'comment_lines_density': 'PERCENT',
+  'comment_lines': 'SHORT_INT',
+  'commented_out_code_lines': 'SHORT_INT',
+  'public_documented_api_density': 'PERCENT',
+  'public_undocumented_api': 'SHORT_INT'
 };
 
 export function formatMeasure (value, metric) {
index 9488219a2a9753a58a60fe25359848d8205b5b9c..967f752fb88840bbd2f9de8824daf0a4c85f9d26 100644 (file)
@@ -9,8 +9,10 @@ export default React.createClass({
         lines = this.props.measures['lines'],
         files = this.props.measures['files'];
 
+    let active = this.props.section === 'size';
+
     return (
-        <Card>
+        <Card linkTo="size" active={active} onRoute={this.props.onRoute}>
           <div className="measures">
             <div className="measure measure-big" data-metric="lines">
               <span className="measure-value">
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js b/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js
new file mode 100644 (file)
index 0000000..1a1dcef
--- /dev/null
@@ -0,0 +1,21 @@
+function hasRightDomain (metric, domains) {
+  return domains.indexOf(metric.domain) !== -1;
+}
+
+function isNotHidden (metric) {
+  return !metric.hidden;
+}
+
+function hasSimpleType (metric) {
+  return metric.type !== 'DATA' && metric.type !== 'DISTRIB';
+}
+
+function isNotDifferential (metric) {
+  return metric.key.indexOf('new_') !== 0;
+}
+
+export function filterMetricsForDomains (metrics, domains) {
+  return metrics.filter(metric => {
+    return hasRightDomain(metric, domains) && isNotHidden(metric) && hasSimpleType(metric) && isNotDifferential(metric);
+  });
+}
index b37c420abcfeff2f81290af7fe5bbbcef2ff6e82..f68dae5458e4a6ec0cf71858630c91ecd48656e7 100644 (file)
-import _ from 'underscore';
 import React from 'react';
-import { BubbleChart } from '../../../components/charts/bubble-chart';
-import { getProjectUrl } from '../../../helpers/Url';
-import { getFiles } from '../../../api/components';
-
-
-const X_METRIC = 'violations';
-const Y_METRIC = 'sqale_index';
-const SIZE_METRIC_1 = 'blocker_violations';
-const SIZE_METRIC_2 = 'critical_violations';
-const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC_1, SIZE_METRIC_2];
-const HEIGHT = 360;
-
-
-function formatIssues (d) {
-  return window.formatMeasure(d, 'SHORT_INT');
-}
-
-function formatDebt (d) {
-  return window.formatMeasure(d, 'SHORT_WORK_DUR');
-}
-
-function getMeasure (component, metric) {
-  return component.measures[metric] || 0;
-}
+import { DomainBubbleChart } from '../domain/bubble-chart';
 
 
 export class IssuesBubbleChart extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, files: [] };
-  }
-
-  componentDidMount () {
-    this.requestFiles();
-  }
-
-  requestFiles () {
-    return getFiles(this.props.component.key, COMPONENTS_METRICS).then(r => {
-      let files = r.map(file => {
-        let measures = {};
-        (file.msr || []).forEach(measure => {
-          measures[measure.key] = measure.val;
-        });
-        return _.extend(file, { measures });
-      });
-      this.setState({ loading: false, files });
-    });
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderBubbleChart () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let items = this.state.files.map(component => {
-      return {
-        x: getMeasure(component, X_METRIC),
-        y: getMeasure(component, Y_METRIC),
-        size: getMeasure(component, SIZE_METRIC_1) + getMeasure(component, SIZE_METRIC_2),
-        link: getProjectUrl(component.key)
-      };
-    });
-    let xGrid = this.state.files.map(component => component.measures[X_METRIC]);
-    let tooltips = this.state.files.map(component => {
-      let inner = [
-        component.name,
-        `Issues: ${formatIssues(getMeasure(component, X_METRIC))}`,
-        `Technical Debt: ${formatDebt(getMeasure(component, Y_METRIC))}`,
-        `Blocker & Critical Issues: ${formatIssues(getMeasure(component, SIZE_METRIC_1) + getMeasure(component, SIZE_METRIC_2))}`
-      ].join('<br>');
-      return `<div class="text-left">${inner}</div>`;
-    });
-    return <BubbleChart items={items}
-                        xGrid={xGrid}
-                        tooltips={tooltips}
-                        height={HEIGHT}
-                        padding={[25, 30, 50, 60]}
-                        formatXTick={formatIssues}
-                        formatYTick={formatDebt}/>;
-  }
-
   render () {
-    return <div className="overview-bubble-chart overview-domain-dark">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project Files</h2>
-        <ul className="list-inline small">
-          <li>X: Issues</li>
-          <li>Y: Technical Debt</li>
-          <li>Size: Blocker & Critical Issues</li>
-        </ul>
-      </div>
-      <div>
-        {this.renderBubbleChart()}
-      </div>
-    </div>;
+    return <DomainBubbleChart {...this.props}
+        xMetric="violations"
+        yMetric="sqale_index"
+        sizeMetrics={['blocker_violations', 'critical_violations']}/>;
   }
 }
index 5266dad14ae47e4b61109e30993dab6664bb6741..3ca3f56b1296edfc5b71abca7a0e6ee01c4cefec 100644 (file)
-import _ from 'underscore';
-import moment from 'moment';
 import React from 'react';
 
-import { LineChart } from '../../../components/charts/line-chart';
-import { formatMeasure } from '../formatting';
-import { getTimeMachineData } from '../../../api/time-machine';
-import { getEvents } from '../../../api/events';
+import { DomainTimeline } from '../domain/timeline';
+import { filterMetricsForDomains } from '../helpers/metrics';
 
 
-const ISSUES_METRICS = [
-  'violations',
-  'blocker_violations',
-  'critical_violations',
-  'major_violations',
-  'minor_violations',
-  'info_violations',
-  'confirmed_issues',
-  'false_positive_issues',
-  'open_issues',
-  'reopened_issues'
-];
-
-const DEBT_METRICS = [
-  'sqale_index',
-  'sqale_debt_ratio'
-];
-
-const HEIGHT = 280;
+const DOMAINS = ['Issues', 'Technical Debt'];
 
 
 export class IssuesTimeline extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, currentMetric: ISSUES_METRICS[0] };
-  }
-
-  componentDidMount () {
-    Promise.all([
-      this.requestTimeMachineData(),
-      this.requestEvents()
-    ]).then(() => this.setState({ loading: false }));
-  }
-
-  requestTimeMachineData () {
-    return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
-      let snapshots = r[0].cells.map(cell => {
-        return { date: moment(cell.d).toDate(), value: cell.v[0] };
-      });
-      this.setState({ snapshots });
-    });
-  }
-
-  requestEvents () {
-    return getEvents(this.props.component.key, 'Version').then(r => {
-      let events = r.map(event => {
-        return { version: event.n, date: moment(event.dt).toDate() };
-      });
-      events = _.sortBy(events, 'date');
-      this.setState({ events });
-    });
-  }
-
-  prepareEvents () {
-    let events = this.state.events;
-    let snapshots = this.state.snapshots;
-    return events
-        .map(event => {
-          let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
-          event.value = snapshot && snapshot.value;
-          return event;
-        })
-        .filter(event => event.value != null);
-  }
-
-  handleMetricChange () {
-    let metric = React.findDOMNode(this.refs.metricSelect).value;
-    this.setState({ currentMetric: metric }, this.requestTimeMachineData);
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderLineChart () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let events = this.prepareEvents();
-
-    let data = events.map((event, index) => {
-      return { x: index, y: event.value };
-    });
-
-    let xTicks = events.map(event => event.version.substr(0, 6));
-
-    let xValues = events.map(event => formatMeasure(event.value, this.state.currentMetric));
-
-    // TODO use leak period
-    let backdropConstraints = [
-      this.state.events.length - 2,
-      this.state.events.length - 1
-    ];
-
-    return <LineChart data={data}
-                      xTicks={xTicks}
-                      xValues={xValues}
-                      backdropConstraints={backdropConstraints}
-                      height={HEIGHT}
-                      interpolate="linear"
-                      padding={[25, 30, 50, 30]}/>;
-  }
-
-  renderTimelineMetricSelect () {
-    if (this.state.loading) {
-      return null;
-    }
-
-    let issueOptions = ISSUES_METRICS
-        .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
-    let debtOptions = DEBT_METRICS
-        .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
-
-    return <select ref="metricSelect"
-                   className="overview-timeline-select"
-                   onChange={this.handleMetricChange.bind(this)}
-                   value={this.state.currentMetric}>
-      <optgroup label="Issues">{issueOptions}</optgroup>
-      <optgroup label="Technical Debt">{debtOptions}</optgroup>
-    </select>;
-  }
-
   render () {
-    return <div className="overview-timeline overview-domain-dark">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project History</h2>
-        {this.renderTimelineMetricSelect()}
-      </div>
-      <div>
-        {this.renderLineChart()}
-      </div>
-    </div>;
+    return <DomainTimeline {...this.props}
+        initialMetric="violations"
+        metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
   }
 }
index 665406c41d9e0d93b20b6ce19bd94db4842922e6..6cab86725a0cb0b7c2eab2dbbdab093e7af26684 100644 (file)
@@ -1,99 +1,20 @@
-import _ from 'underscore';
+import d3 from 'd3';
 import React from 'react';
 
-import { Treemap } from '../../../components/charts/treemap';
-import { formatMeasure } from '../formatting';
-import { getChildren } from '../../../api/components';
+import { DomainTreemap } from '../domain/treemap';
 
-const COMPONENTS_METRICS = [
-  'lines',
-  'sqale_rating'
-];
 
-const HEIGHT = 360;
+const COLORS_5 = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
 
-export class IssuesTreemap extends React.Component {
-  constructor () {
-    super();
-    this.state = { loading: true, components: [] };
-  }
-
-  componentDidMount () {
-    this.requestComponents();
-  }
-
-  requestComponents () {
-    return getChildren(this.props.component.key, COMPONENTS_METRICS).then(r => {
-      let components = r.map(component => {
-        let measures = {};
-        component.msr.forEach(measure => {
-          measures[measure.key] = measure.val;
-        });
-        return _.extend(component, { measures });
-      });
-      this.setState({ loading: false, components });
-    });
-  }
-
-  // TODO use css
-  getRatingColor (rating) {
-    switch (rating) {
-      case 1:
-        return '#00AA00';
-      case 2:
-        return '#80CC00';
-      case 3:
-        return '#FFEE00';
-      case 4:
-        return '#F77700';
-      case 5:
-        return '#EE0000';
-      default:
-        return '#777';
-    }
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderTreemap () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    let items = this.state.components.map(component => {
-      return {
-        size: component.measures['lines'],
-        color: this.getRatingColor(component.measures['sqale_rating'])
-      };
-    });
-    let labels = this.state.components.map(component => component.name);
-    let tooltips = this.state.components.map(component => {
-      let inner = [
-        component.name,
-        `Lines: ${formatMeasure(component.measures['lines'], 'lines')}`,
-        `SQALE Rating: ${formatMeasure(component.measures['sqale_rating'], 'sqale_rating')}`
-      ].join('<br>');
-      return `<div class="text-left">${inner}</div>`;
-    });
-    return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
-  }
 
+export class IssuesTreemap extends React.Component {
   render () {
-    return <div className="overview-domain-section overview-treemap">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Project Components</h2>
-        <ul className="list-inline small">
-          <li>Size: Lines</li>
-          <li>Color: SQALE Rating</li>
-        </ul>
-      </div>
-      <div>
-        {this.renderTreemap()}
-      </div>
-    </div>;
+    let scale = d3.scale.ordinal()
+        .domain([1, 2, 3, 4, 5])
+        .range(COLORS_5);
+    return <DomainTreemap {...this.props}
+        sizeMetric="ncloc"
+        colorMetric="sqale_rating"
+        scale={scale}/>;
   }
 }
index c0520880841c9b7d3f168bdcd3457c7bbb44aa66..f2111487d3fd3b16caca9f90d0d0ea75cb6a66de 100644 (file)
@@ -1,11 +1,15 @@
 import React from 'react';
 import offset from 'document-offset';
+
 import GeneralMain from './general/main';
 import IssuesMain from './issues/main';
 import CoverageMain from './coverage/main';
 import DuplicationsMain from './duplications/main';
+import SizeMain from './size/main';
 import Meta from './meta';
 
+import { getMetrics } from '../../api/metrics';
+
 export default class Overview extends React.Component {
   constructor () {
     super();
@@ -13,6 +17,14 @@ export default class Overview extends React.Component {
     this.state = { section: hash.length ? hash.substr(1) : null };
   }
 
+  componentDidMount () {
+    this.requestMetrics();
+  }
+
+  requestMetrics () {
+    return getMetrics().then(metrics => this.setState({ metrics }));
+  }
+
   handleRoute (section, el) {
     this.setState({ section }, () => this.scrollToEl(el));
     window.location.href = '#' + section;
@@ -24,16 +36,23 @@ export default class Overview extends React.Component {
   }
 
   render () {
+    if (!this.state.metrics) {
+      return null;
+    }
+
     let child;
     switch (this.state.section) {
       case 'issues':
-        child = <IssuesMain {...this.props}/>;
+        child = <IssuesMain {...this.props} {...this.state}/>;
         break;
       case 'coverage':
-        child = <CoverageMain {...this.props}/>;
+        child = <CoverageMain {...this.props} {...this.state}/>;
         break;
       case 'duplications':
-        child = <DuplicationsMain {...this.props}/>;
+        child = <DuplicationsMain {...this.props} {...this.state}/>;
+        break;
+      case 'size':
+        child = <SizeMain {...this.props} {...this.state}/>;
         break;
       default:
         child = null;
diff --git a/server/sonar-web/src/main/js/apps/overview/size/comments-details.js b/server/sonar-web/src/main/js/apps/overview/size/comments-details.js
new file mode 100644 (file)
index 0000000..1f20582
--- /dev/null
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import { DomainMeasuresList } from '../domain/measures-list';
+
+
+const METRICS = [
+  'comment_lines',
+  'comment_lines_density'
+];
+
+
+export class CommentsDetails extends React.Component {
+  render () {
+    return <div className="overview-domain-section">
+      <h2 className="overview-title">Comments</h2>
+      <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/complexity-details.js b/server/sonar-web/src/main/js/apps/overview/size/complexity-details.js
new file mode 100644 (file)
index 0000000..bd1ba93
--- /dev/null
@@ -0,0 +1,21 @@
+import React from 'react';
+
+import { DomainMeasuresList } from '../domain/measures-list';
+
+
+const METRICS = [
+  'complexity',
+  'class_complexity',
+  'file_complexity',
+  'function_complexity'
+];
+
+
+export class ComplexityDetails extends React.Component {
+  render () {
+    return <div className="overview-domain-section">
+      <h2 className="overview-title">Complexity</h2>
+      <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js b/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js
new file mode 100644 (file)
index 0000000..8ff3da2
--- /dev/null
@@ -0,0 +1,68 @@
+import React from 'react';
+
+import { BarChart } from '../../../components/charts/bar-chart';
+import { getMeasures } from '../../../api/measures';
+
+
+const HEIGHT = 120;
+const COMPLEXITY_DISTRIBUTION_METRIC = 'file_complexity_distribution';
+
+
+export class ComplexityDistribution extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = { loading: true };
+  }
+
+  componentDidMount () {
+    this.requestData();
+  }
+
+  requestData () {
+    return getMeasures(this.props.component.key, [COMPLEXITY_DISTRIBUTION_METRIC]).then(measures => {
+      this.setState({ loading: false, distribution: measures[COMPLEXITY_DISTRIBUTION_METRIC] });
+    });
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderBarChart () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let data = this.state.distribution.split(';').map((point, index) => {
+      let tokens = point.split('=');
+      return { x: index, y: parseInt(tokens[1], 10), value: parseInt(tokens[0], 10) };
+    });
+
+    let xTicks = data.map(point => point.value);
+
+    let xValues = data.map(point => window.formatMeasure(point.y, 'INT'));
+
+    return <BarChart data={data}
+                     xTicks={xTicks}
+                     xValues={xValues}
+                     height={HEIGHT}
+                     padding={[25, 30, 50, 30]}/>;
+  }
+
+  render () {
+    return <div className="overview-bar-chart">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">&nbsp;</h2>
+        <ul className="list-inline small">
+          <li>X: Complexity/file</li>
+          <li>Size: Number of Files</li>
+        </ul>
+      </div>
+      <div>
+        {this.renderBarChart()}
+      </div>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js b/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js
new file mode 100644 (file)
index 0000000..ab72492
--- /dev/null
@@ -0,0 +1,81 @@
+import _ from 'underscore';
+import React from 'react';
+
+import { BarChart } from '../../../components/charts/bar-chart';
+import { getMeasures } from '../../../api/measures';
+import { getLanguages } from '../../../api/languages';
+
+
+const HEIGHT = 180;
+const COMPLEXITY_DISTRIBUTION_METRIC = 'ncloc_language_distribution';
+
+
+export class LanguageDistribution extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = { loading: true };
+  }
+
+  componentDidMount () {
+    this.requestData();
+  }
+
+  requestData () {
+    return Promise.all([
+      getMeasures(this.props.component.key, [COMPLEXITY_DISTRIBUTION_METRIC]),
+      getLanguages()
+    ]).then(responses => {
+      this.setState({
+        loading: false,
+        distribution: responses[0][COMPLEXITY_DISTRIBUTION_METRIC],
+        languages: responses[1]
+      });
+    });
+  }
+
+  getLanguageName (langKey) {
+    let lang = _.findWhere(this.state.languages, { key: langKey });
+    return lang ? lang.name : window.t('unknown');
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderBarChart () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let data = this.state.distribution.split(';').map((d, index) => {
+      let tokens = d.split('=');
+      return { x: index, y: parseInt(tokens[1], 10), lang: tokens[0] };
+    });
+
+    let xTicks = data.map(d => this.getLanguageName(d.lang));
+
+    let xValues = data.map(d => window.formatMeasure(d.y, 'INT'));
+
+    return <BarChart data={data}
+                     xTicks={xTicks}
+                     xValues={xValues}
+                     height={HEIGHT}
+                     padding={[25, 30, 50, 30]}/>;
+  }
+
+  render () {
+    return <div className="overview-bar-chart">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">&nbsp;</h2>
+        <ul className="list-inline small">
+          <li>Size: Lines of Code</li>
+        </ul>
+      </div>
+      <div>
+        {this.renderBarChart()}
+      </div>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/main.js b/server/sonar-web/src/main/js/apps/overview/size/main.js
new file mode 100644 (file)
index 0000000..72f892c
--- /dev/null
@@ -0,0 +1,44 @@
+import React from 'react';
+
+import { SizeTimeline } from './timeline';
+import { SizeDetails } from './size-details';
+import { ComplexityDetails } from './complexity-details';
+import { CommentsDetails } from './comments-details';
+import { ComplexityDistribution } from './complexity-distribution';
+import { LanguageDistribution } from './language-distribution';
+import { SizeTreemap } from './treemap';
+
+
+export default class extends React.Component {
+  render () {
+    return <div className="overview-domain">
+      <SizeTimeline {...this.props}/>
+
+      <div className="flex-columns">
+        <div className="flex-column flex-column-third">
+          <SizeDetails {...this.props}/>
+        </div>
+        <div className="flex-column flex-column-two-thirds">
+          <LanguageDistribution {...this.props}/>
+        </div>
+      </div>
+
+      <div className="flex-columns">
+        <div className="flex-column flex-column-third">
+          <ComplexityDetails {...this.props}/>
+        </div>
+        <div className="flex-column flex-column-two-thirds">
+          <ComplexityDistribution {...this.props}/>
+        </div>
+      </div>
+
+      <div className="flex-columns">
+        <div className="flex-column flex-column-third">
+          <CommentsDetails {...this.props}/>
+        </div>
+      </div>
+
+      <SizeTreemap {...this.props}/>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/size-details.js b/server/sonar-web/src/main/js/apps/overview/size/size-details.js
new file mode 100644 (file)
index 0000000..c736224
--- /dev/null
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { DomainMeasuresList } from '../domain/measures-list';
+
+
+const METRICS = [
+  'ncloc',
+  'lines',
+  'files',
+  'directories',
+  'functions',
+  'classes',
+  'accessors'
+];
+
+
+export class SizeDetails extends React.Component {
+  render () {
+    return <div className="overview-domain-section">
+      <h2 className="overview-title">Size</h2>
+      <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/timeline.js b/server/sonar-web/src/main/js/apps/overview/size/timeline.js
new file mode 100644 (file)
index 0000000..2197872
--- /dev/null
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { DomainTimeline } from '../domain/timeline';
+import { filterMetricsForDomains } from '../helpers/metrics';
+
+
+const DOMAINS = ['Size', 'Complexity', 'Documentation'];
+
+
+export class SizeTimeline extends React.Component {
+  render () {
+    return <DomainTimeline {...this.props}
+        initialMetric="ncloc"
+        metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/treemap.js b/server/sonar-web/src/main/js/apps/overview/size/treemap.js
new file mode 100644 (file)
index 0000000..ddfc267
--- /dev/null
@@ -0,0 +1,10 @@
+import React from 'react';
+
+import { DomainTreemap } from '../domain/treemap';
+
+
+export class SizeTreemap extends React.Component {
+  render () {
+    return <DomainTreemap {...this.props} sizeMetric="ncloc"/>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js
new file mode 100644 (file)
index 0000000..347d45d
--- /dev/null
@@ -0,0 +1,107 @@
+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 };
+  }
+
+  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 });
+  }
+
+  renderXTicks (xScale, yScale) {
+    if (!this.props.xTicks.length) {
+      return null;
+    }
+    let ticks = this.props.xTicks.map((tick, index) => {
+      let point = this.props.data[index];
+      let x = Math.round(xScale(point.x) + xScale.rangeBand() / 2 + this.props.barsWidth / 2);
+      let y = yScale.range()[0];
+      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) {
+      return null;
+    }
+    let ticks = this.props.xValues.map((value, index) => {
+      let point = this.props.data[index];
+      let x = Math.round(xScale(point.x) + xScale.rangeBand() / 2 + this.props.barsWidth / 2);
+      let y = yScale(point.y);
+      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) => {
+      let x = Math.round(xScale(d.x) + xScale.rangeBand() / 2);
+      let maxY = yScale.range()[0];
+      let y = Math.round(yScale(d.y)) - /* minimum bar height */ 1;
+      let height = maxY - y;
+      return <rect key={index} className="bar-chart-bar"
+                   x={x} y={y} width={this.props.barsWidth} height={height}/>;
+    });
+    return <g>{bars}</g>;
+  }
+
+  render () {
+    if (!this.state.width || !this.state.height) {
+      return <div/>;
+    }
+
+    let availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3];
+    let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
+
+    let maxY = d3.max(this.props.data, d => d.y);
+    let xScale = d3.scale.ordinal()
+                   .domain(this.props.data.map(d => d.x))
+                   .rangeRoundBands([0, availableWidth]);
+    let yScale = d3.scale.linear()
+                   .domain([0, maxY])
+                   .range([availableHeight, 0]);
+
+    return <svg className="bar-chart" width={this.state.width} height={this.state.height}>
+      <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+        {this.renderXTicks(xScale, yScale)}
+        {this.renderXValues(xScale, yScale)}
+        {this.renderBars(xScale, yScale)}
+      </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 e2c5d200f577d623f309658c0972f2599be0271a..82edf22ce4eb3239afef0787c2f293567078634d 100644 (file)
@@ -172,9 +172,8 @@ export class BubbleChart extends React.Component {
 
     let bubbles = this.props.items
         .map((item, index) => {
-          let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null;
           return <Bubble key={index}
-                         tooltip={tooltip}
+                         tooltip={item.tooltip}
                          link={item.link}
                          x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>;
         });
index db6ceb8d364ad29c8d4c3eaea9e92c78fbf84d69..d00ce0be0fce49a20c427e4b6a2e78a3605c4c7d 100644 (file)
@@ -111,13 +111,11 @@ export class Treemap extends React.Component {
         .nodes({ children: this.props.items })
         .filter(d => !d.children);
 
-    let prefix = mostCommitPrefix(this.props.labels),
+    let prefix = mostCommitPrefix(this.props.items.map(item => item.label)),
         prefixLength = prefix.length;
 
     let rectangles = nodes.map((node, index) => {
-      let label = prefixLength ? `${prefix}<br>${this.props.labels[index].substr(prefixLength)}` :
-          this.props.labels[index];
-      let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null;
+      let label = prefixLength ? `${prefix}<br>${node.label.substr(prefixLength)}` : node.label;
       return <TreemapRect key={index}
                           x={node.x}
                           y={node.y}
@@ -126,7 +124,7 @@ export class Treemap extends React.Component {
                           fill={node.color}
                           label={label}
                           prefix={prefix}
-                          tooltip={tooltip}/>;
+                          tooltip={node.tooltip}/>;
     });
 
     return <div className="sonar-d3">
@@ -138,6 +136,5 @@ export class Treemap extends React.Component {
 }
 
 Treemap.propTypes = {
-  labels: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
-  tooltips: React.PropTypes.arrayOf(React.PropTypes.string)
+  items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
 };
index 4e0edc3fcbaeed86f7d362478f79c079c1c48d8a..9b43ba81fdfb6a486a0f6cd9a0995d4c15c39a83 100644 (file)
   }
 }
 
+.overview-bar-chart {
+  .bar-chart-bar {
+    fill: @blue;
+  }
+
+  .bar-chart-tick {
+    fill: @baseFontColor;
+    font-size: 11px;
+    text-anchor: middle;
+  }
+}
+
 .overview-treemap {
   .overview-domain-header {
+    padding-top: 0;
     padding-left: 0;
     padding-right: 0;
   }