From b9a9951a8a7f641b35761d5e91d1c2bd281b3db1 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 19 Sep 2017 16:53:16 +0200 Subject: [PATCH] SONAR-9841 display tooltip on language distribution chart --- server/sonar-web/package.json | 2 + .../components/MeasureHeader.js | 1 + .../main/js/apps/overview/meta/MetaSize.js | 2 +- .../js/apps/portfolio/components/Summary.tsx | 4 +- .../__snapshots__/Summary-test.tsx.snap | 6 +- .../main/js/components/charts/Histogram.tsx | 146 ++++++++ .../charts/LanguageDistribution.tsx | 35 +- .../charts/__tests__/Histogram-test.tsx | 68 ++++ .../__tests__/LanguageDistribution-test.tsx | 34 ++ .../__snapshots__/Histogram-test.tsx.snap | 319 ++++++++++++++++++ .../LanguageDistribution-test.tsx.snap | 48 +++ .../main/js/components/charts/histogram.js | 166 --------- server/sonar-web/yarn.lock | 14 + 13 files changed, 657 insertions(+), 188 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/charts/Histogram.tsx create mode 100644 server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx create mode 100644 server/sonar-web/src/main/js/components/charts/__tests__/LanguageDistribution-test.tsx create mode 100644 server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/components/charts/histogram.js diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index aecf565d6c1..4c8d13681cb 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -46,6 +46,8 @@ }, "devDependencies": { "@types/classnames": "2.2.0", + "@types/d3-array": "1.2.1", + "@types/d3-scale": "1.0.10", "@types/date-fns": "2.6.0", "@types/enzyme": "2.8.6", "@types/escape-html": "0.0.19", diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js index de343e95337..49f039c35d1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js @@ -140,6 +140,7 @@ export default class MeasureHeader extends React.PureComponent { )} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js index a488f469da6..7cd41c45753 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js @@ -57,7 +57,7 @@ export default class MetaSize extends React.PureComponent { return languageDistribution ? (
- +
) : null; }; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx index aa229597a1e..5aecf73d70f 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx @@ -60,8 +60,8 @@ export default function Summary({ component, measures }: Props) { {nclocDistribution && ( -
- +
+
)} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap index d377a6a44a9..db159a6947a 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap @@ -82,14 +82,10 @@ exports[`renders 1`] = `
diff --git a/server/sonar-web/src/main/js/components/charts/Histogram.tsx b/server/sonar-web/src/main/js/components/charts/Histogram.tsx new file mode 100644 index 00000000000..420de6946ad --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/Histogram.tsx @@ -0,0 +1,146 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { max } from 'd3-array'; +import { scaleLinear, scaleBand, ScaleLinear, ScaleBand } from 'd3-scale'; +import Tooltip from '../controls/Tooltip'; + +interface Props { + alignTicks?: boolean; + bars: number[]; + height: number; + padding?: [number, number, number, number]; + yTicks?: string[]; + yTooltips?: string[]; + yValues?: string[]; + width: number; +} + +const BAR_HEIGHT = 10; +const DEFAULT_PADDING = [10, 10, 10, 10]; + +type XScale = ScaleLinear; +type YScale = ScaleBand; + +export default class Histogram extends React.PureComponent { + wrapWithTooltip(element: React.ReactNode, index: number) { + const tooltip = this.props.yTooltips && this.props.yTooltips[index]; + return tooltip ? ( + + {element} + + ) : ( + element + ); + } + + renderBar(d: number, index: number, xScale: XScale, yScale: YScale) { + const { alignTicks, padding = DEFAULT_PADDING } = this.props; + + const width = Math.round(xScale(d)) + /* minimum bar width */ 1; + const x = xScale.range()[0] + (alignTicks ? padding[3] : 0); + const y = Math.round(yScale(index)! + yScale.bandwidth() / 2); + + return ; + } + + renderValue(d: number, index: number, xScale: XScale, yScale: YScale) { + const { alignTicks, padding = DEFAULT_PADDING, yValues } = this.props; + + const value = yValues && yValues[index]; + + if (!value) { + return null; + } + + const x = xScale(d) + (alignTicks ? padding[3] : 0); + const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2); + + return this.wrapWithTooltip( + + {value} + , + index + ); + } + + renderTick(index: number, xScale: XScale, yScale: YScale) { + const { alignTicks, yTicks } = this.props; + + const tick = yTicks && yTicks[index]; + + if (!tick) { + return null; + } + + const x = xScale.range()[0]; + const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2); + const historyTickClass = alignTicks ? 'histogram-tick-start' : 'histogram-tick'; + + return ( + + {tick} + + ); + } + + renderBars(xScale: XScale, yScale: YScale) { + return ( + + {this.props.bars.map((d, index) => { + return ( + + {this.renderBar(d, index, xScale, yScale)} + {this.renderValue(d, index, xScale, yScale)} + {this.renderTick(index, xScale, yScale)} + + ); + })} + + ); + } + + render() { + const { bars, width, height, padding = DEFAULT_PADDING } = this.props; + + const availableWidth = width - padding[1] - padding[3]; + const xScale: XScale = scaleLinear() + .domain([0, max(bars)!]) + .range([0, availableWidth]); + + const availableHeight = height - padding[0] - padding[2]; + const yScale: YScale = scaleBand() + .domain(bars.map((_, index) => index)) + .rangeRound([0, availableHeight]); + + return ( + + + {this.renderBars(xScale, yScale)} + + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx b/server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx index 6766372207f..d3f769728e6 100644 --- a/server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx +++ b/server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { find, sortBy } from 'lodash'; -import { Histogram } from './histogram'; +import Histogram from './Histogram'; import { formatMeasure } from '../../helpers/measures'; import { Language } from '../../api/languages'; import { translate } from '../../helpers/l10n'; @@ -28,37 +28,44 @@ interface Props { alignTicks?: boolean; distribution: string; languages?: Language[]; + width: number; } export default function LanguageDistribution(props: Props) { - let data = props.distribution.split(';').map((point, index) => { + let distribution = props.distribution.split(';').map(point => { const tokens = point.split('='); - return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] }; + return { language: tokens[0], lines: parseInt(tokens[1], 10) }; }); - data = sortBy(data, d => -d.x); + distribution = sortBy(distribution, d => -d.lines); - const yTicks = data.map(point => getLanguageName(point.value)).map(cutLanguageName); - const yValues = data.map(point => formatMeasure(point.x, 'SHORT_INT')); + const data = distribution.map(d => d.lines); + const yTicks = distribution.map(d => getLanguageName(d.language)).map(cutLanguageName); + const yTooltips = distribution.map(d => (d.lines > 1000 ? formatMeasure(d.lines, 'INT') : '')); + const yValues = distribution.map(d => formatMeasure(d.lines, 'SHORT_INT')); return ( ); function getLanguageName(langKey: string) { + if (langKey === '') { + return translate('unknown'); + } const lang = find(props.languages, { key: langKey }); - return lang ? lang.name : translate('unknown'); + return lang ? lang.name : langKey; } +} - function cutLanguageName(name: string) { - return name.length > 10 ? `${name.substr(0, 7)}...` : name; - } +function cutLanguageName(name: string) { + return name.length > 10 ? `${name.substr(0, 7)}...` : name; } diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx new file mode 100644 index 00000000000..9db4b13536a --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Histogram from '../Histogram'; + +it('renders', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('renders with yValues', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('renders with yValues and yTicks', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('renders with yValues, yTicks and yTooltips', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/LanguageDistribution-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/LanguageDistribution-test.tsx new file mode 100644 index 00000000000..8bed7941d2f --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/__tests__/LanguageDistribution-test.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import LanguageDistribution from '../LanguageDistribution'; + +it('renders', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap new file mode 100644 index 00000000000..e7885c89cad --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap @@ -0,0 +1,319 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + + + + + + + + + + + + + + + +`; + +exports[`renders with yValues 1`] = ` + + + + + + + 100.0 + + + + + + 75.0 + + + + + + 150.0 + + + + + +`; + +exports[`renders with yValues and yTicks 1`] = ` + + + + + + + 100.0 + + + a + + + + + + 75.0 + + + b + + + + + + 150.0 + + + c + + + + + +`; + +exports[`renders with yValues, yTicks and yTooltips 1`] = ` + + + + + + + + 100.0 + + + + a + + + + + + + 75.0 + + + + b + + + + + + + 150.0 + + + + c + + + + + +`; diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap new file mode 100644 index 00000000000..1d2a6e2c0eb --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/components/charts/histogram.js b/server/sonar-web/src/main/js/components/charts/histogram.js deleted file mode 100644 index 2201cf025c0..00000000000 --- a/server/sonar-web/src/main/js/components/charts/histogram.js +++ /dev/null @@ -1,166 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import { max } from 'd3-array'; -import { scaleLinear, scaleBand } from 'd3-scale'; -import { ResizeMixin } from './../mixins/resize-mixin'; -import { TooltipsMixin } from './../mixins/tooltips-mixin'; - -export const Histogram = createReactClass({ - displayName: 'Histogram', - - propTypes: { - alignTicks: PropTypes.bool, - data: PropTypes.arrayOf(PropTypes.object).isRequired, - yTicks: PropTypes.arrayOf(PropTypes.any), - yValues: PropTypes.arrayOf(PropTypes.any), - width: PropTypes.number, - height: PropTypes.number, - padding: PropTypes.arrayOf(PropTypes.number), - barsHeight: PropTypes.number, - onBarClick: PropTypes.func - }, - - mixins: [ResizeMixin, TooltipsMixin], - - getDefaultProps() { - return { - xTicks: [], - xValues: [], - padding: [10, 10, 10, 10], - barsHeight: 10 - }; - }, - - getInitialState() { - return { width: this.props.width, height: this.props.height }; - }, - - handleClick(point) { - this.props.onBarClick(point); - }, - - renderTicks(xScale, yScale) { - if (!this.props.yTicks.length) { - return null; - } - const ticks = this.props.yTicks.map((tick, index) => { - const point = this.props.data[index]; - const x = xScale.range()[0]; - const y = Math.round(yScale(point.y) + yScale.bandwidth() / 2 + this.props.barsHeight / 2); - const label = tick.label ? tick.label : tick; - const tooltip = tick.tooltip ? tick.tooltip : null; - const historyTickClass = this.props.alignTicks ? 'histogram-tick-start' : 'histogram-tick'; - return ( - - {label} - - ); - }); - return {ticks}; - }, - - renderValues(xScale, yScale) { - if (!this.props.yValues.length) { - return null; - } - const ticks = this.props.yValues.map((value, index) => { - const point = this.props.data[index]; - const x = xScale(point.x) + (this.props.alignTicks ? this.props.padding[3] : 0); - const y = Math.round(yScale(point.y) + yScale.bandwidth() / 2 + this.props.barsHeight / 2); - return ( - - {value} - - ); - }); - return {ticks}; - }, - - renderBars(xScale, yScale) { - const bars = this.props.data.map((d, index) => { - const width = Math.round(xScale(d.x)) + /* minimum bar width */ 1; - const x = xScale.range()[0] + (this.props.alignTicks ? this.props.padding[3] : 0); - const y = Math.round(yScale(d.y) + yScale.bandwidth() / 2); - return ( - - ); - }); - return {bars}; - }, - - render() { - if (!this.state.width || !this.state.height) { - return
; - } - - const availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3]; - const availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2]; - - const maxX = max(this.props.data, d => d.x); - const xScale = scaleLinear() - .domain([0, maxX]) - .range([0, availableWidth]); - const yScale = scaleBand() - .domain(this.props.data.map(d => d.y)) - .rangeRound([0, availableHeight]); - - return ( - - - {this.renderTicks(xScale, yScale)} - {this.renderValues(xScale, yScale)} - {this.renderBars(xScale, yScale)} - - - ); - } -}); diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 9c85f40091f..a7f70bc6202 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -17,6 +17,20 @@ version "2.2.0" resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.0.tgz#f2312039e780bdf89d7d4102a26ec11de5ec58aa" +"@types/d3-array@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.1.tgz#e489605208d46a1c9d980d2e5772fa9c75d9ec65" + +"@types/d3-scale@1.0.10": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-1.0.10.tgz#8c5c1dca54a159eed042b46719dbb3bdb7e8c842" + dependencies: + "@types/d3-time" "*" + +"@types/d3-time@*": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.7.tgz#4266d7c9be15fa81256a88d1d052d61cd8dc572c" + "@types/date-fns@2.6.0": version "2.6.0" resolved "https://registry.yarnpkg.com/@types/date-fns/-/date-fns-2.6.0.tgz#b062ca46562002909be0c63a6467ed173136acc1" -- 2.39.5