From d6652afabd60eee53364a30b8900d2885f43fcf3 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 26 Oct 2015 16:23:18 +0100 Subject: [PATCH] SONAR-6357 improve display of chart on the project overview page --- .../overview/coverage/coverage-details.js | 29 +++- .../apps/overview/coverage/tests-details.js | 67 +------- .../js/apps/overview/domain/bubble-chart.js | 2 - .../js/apps/overview/domain/measures-list.js | 19 ++- .../main/js/apps/overview/domain/timeline.js | 10 ++ .../duplications/duplications-details.js | 50 +----- .../main/js/components/charts/bar-chart.js | 69 ++++---- .../main/js/components/charts/bubble-chart.js | 155 ++++++++---------- .../main/js/components/charts/line-chart.js | 114 ++++++------- .../components/charts/mixins/resize-mixin.js | 27 +++ .../charts/mixins/tooltips-mixin.js | 17 ++ .../src/main/js/components/charts/timeline.js | 85 ---------- .../src/main/js/components/charts/treemap.js | 90 ++++------ .../main/js/components/charts/word-cloud.js | 50 +++--- 14 files changed, 305 insertions(+), 479 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js create mode 100644 server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js delete mode 100644 server/sonar-web/src/main/js/components/charts/timeline.js diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js b/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js index 403d8b69120..a6510d60faf 100644 --- a/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js +++ b/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js @@ -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 + {window.formatMeasure(value, 'PERCENT')} + ; + } else { + return '—'; + } + } + + renderCoverage (coverage, lineCoverage, branchCoverage, prefix) { return @@ -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'], + '')} ; } @@ -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_')} ; } @@ -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_')} ; } diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js b/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js index 65034729d79..0bc37699561 100644 --- a/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js +++ b/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js @@ -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
-

Tests Details

-
Coverage - {formatCoverage(coverage)} + {this.renderValue(coverage, prefix + 'coverage')}
Line Coverage - {formatCoverage(lineCoverage)} + {this.renderValue(lineCoverage, prefix + 'line_coverage')}
Branch Coverage - {formatCoverage(branchCoverage)} + {this.renderValue(branchCoverage, prefix + 'branch_coverage')}
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Tests - {format(this.state.measures['tests'], 'tests')} -
Skipped Tests - {format(this.state.measures['skipped_tests'], 'skipped_tests')} -
Test Errors - {format(this.state.measures['test_errors'], 'test_errors')} -
Test Failures - {format(this.state.measures['test_failures'], 'test_failures')} -
Tests Execution Time - {format(this.state.measures['test_execution_time'], 'test_execution_time')} -
Tests Success - {format(this.state.measures['test_success_density'], 'test_success_density')} -
+

Tests

+ ; } } 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 index 730795e11ac..6adef652fe6 100644 --- 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 @@ -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 + {window.formatMeasure(value, metricType)} + ; + } else { + return '—'; + } + } + render () { let rows = this.props.metricsToDisplay.map(metric => { let metricObject = this.getMetricObject(metric); return {metricObject.name} - - {format(this.state.measures[metric], metricObject.type)} - + {this.renderValue(this.state.measures[metric], metric, metricObject.type)} ; }); 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 index ff671f1fc71..7fdee332a63 100644 --- a/server/sonar-web/src/main/js/apps/overview/domain/timeline.js +++ b/server/sonar-web/src/main/js/apps/overview/domain/timeline.js @@ -79,11 +79,21 @@ export class DomainTimeline extends React.Component { ; } + renderWhenNoHistoricalData () { + return
+ There is no historical data. +
; + } + 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; diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js b/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js index dace4233130..b1b9ec79138 100644 --- a/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js +++ b/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js @@ -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
-

Details

- - - - - - - - - - - - - - - - - - - -
Duplications - {formatMeasure(this.state.measures['duplicated_lines_density'], 'duplicated_lines_density')} -
Blocks - {formatMeasure(this.state.measures['duplicated_blocks'], 'duplicated_blocks')} -
Files - {formatMeasure(this.state.measures['duplicated_files'], 'duplicated_files')} -
Lines - {formatMeasure(this.state.measures['duplicated_lines'], 'duplicated_lines')} -
+

Duplications

+
; } } 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 index 347d45d9673..c7444fedcb9 100644 --- a/server/sonar-web/src/main/js/components/charts/bar-chart.js +++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js @@ -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 {tick}; }); return {ticks}; - } + }, renderXValues (xScale, yScale) { if (!this.props.xValues.length) { @@ -51,7 +53,7 @@ export class BarChart extends React.Component { return {value}; }); return {ticks}; - } + }, 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 {bars}; - } + }, render () { if (!this.state.width || !this.state.height) { @@ -89,19 +91,4 @@ export class BarChart extends React.Component { ; } -} - -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 -}; +}); diff --git a/server/sonar-web/src/main/js/components/charts/bubble-chart.js b/server/sonar-web/src/main/js/components/charts/bubble-chart.js index 82edf22ce4e..87f7ef84f5a 100644 --- a/server/sonar-web/src/main/js/components/charts/bubble-chart.js +++ b/server/sonar-web/src/main/js/components/charts/bubble-chart.js @@ -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 ; } -} - - -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 {lines}; - } + }, renderYGrid (xScale, yScale) { if (!this.props.displayYGrid) { @@ -110,7 +118,7 @@ export class BubbleChart extends React.Component { }); return {lines}; - } + }, renderXTicks (xScale, yScale) { if (!this.props.displayXTicks) { @@ -127,7 +135,7 @@ export class BubbleChart extends React.Component { }); return {ticks}; - } + }, renderYTicks (xScale, yScale) { if (!this.props.displayYTicks) { @@ -145,7 +153,7 @@ export class BubbleChart extends React.Component { }); return {ticks}; - } + }, 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 ; - }); + .map((item, index) => { + return ; + }); return @@ -188,25 +196,4 @@ export class BubbleChart extends React.Component { ; } -} - -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 -}; +}); diff --git a/server/sonar-web/src/main/js/components/charts/line-chart.js b/server/sonar-web/src/main/js/components/charts/line-chart.js index bbb14f3a69d..a92ea08cab4 100644 --- a/server/sonar-web/src/main/js/components/charts/line-chart.js +++ b/server/sonar-web/src/main/js/components/charts/line-chart.js @@ -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 ; - } + }, renderPoints (xScale, yScale) { if (!this.props.displayPoints) { @@ -58,7 +68,7 @@ export class LineChart extends React.Component { return ; }); return {points}; - } + }, renderVerticalGrid (xScale, yScale) { if (!this.props.displayVerticalGrid) { @@ -71,7 +81,7 @@ export class LineChart extends React.Component { return ; }); return {lines}; - } + }, renderXTicks (xScale, yScale) { if (!this.props.xTicks.length) { @@ -84,7 +94,7 @@ export class LineChart extends React.Component { return {tick}; }); return {ticks}; - } + }, renderXValues (xScale, yScale) { if (!this.props.xValues.length) { @@ -97,16 +107,16 @@ export class LineChart extends React.Component { return {value}; }); return {ticks}; - } + }, 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 ; - } + }, 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 @@ -135,22 +145,4 @@ export class LineChart extends React.Component { ; } -} - -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 index 00000000000..206cb90f355 --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js @@ -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 index 00000000000..2b3fc24346e --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js @@ -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 index d200ff154cf..00000000000 --- a/server/sonar-web/src/main/js/components/charts/timeline.js +++ /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 ; - } - - 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 ; - } - - render () { - if (!this.state.width || !this.state.height) { - return
; - } - - 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 - - {this.renderBackdrop(xScale, yScale, maxY)} - {this.renderLine(xScale, yScale)} - - ; - } -} - -Timeline.defaultProps = { - lineWidth: 2, - displayBackdrop: true, - interpolate: 'basis' -}; - -Timeline.propTypes = { - snapshots: React.PropTypes.arrayOf(React.PropTypes.object).isRequired -}; diff --git a/server/sonar-web/src/main/js/components/charts/treemap.js b/server/sonar-web/src/main/js/components/charts/treemap.js index d00ce0be0fc..51b140e003d 100644 --- a/server/sonar-web/src/main/js/components/charts/treemap.js +++ b/server/sonar-web/src/main/js/components/charts/treemap.js @@ -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 }}/>
; } -} +}); -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 { ; } -} - -Treemap.propTypes = { - items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired -}; +}); diff --git a/server/sonar-web/src/main/js/components/charts/word-cloud.js b/server/sonar-web/src/main/js/components/charts/word-cloud.js index e2382d9e035..53c36ee76af 100644 --- a/server/sonar-web/src/main/js/components/charts/word-cloud.js +++ b/server/sonar-web/src/main/js/components/charts/word-cloud.js @@ -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 {this.props.text}; } -} +}); -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) => ); return
{words}
; } -} - -WordCloud.defaultProps = { - sizeRange: [10, 24] -}; - -WordCloud.propTypes = { - sizeRange: React.PropTypes.arrayOf(React.PropTypes.number) -}; +}); -- 2.39.5