Browse Source

SONAR-9841 display tooltip on language distribution chart

tags/6.6-RC1
Stas Vilchik 6 years ago
parent
commit
b9a9951a8a

+ 2
- 0
server/sonar-web/package.json View File

@@ -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",

+ 1
- 0
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js View File

@@ -140,6 +140,7 @@ export default class MeasureHeader extends React.PureComponent {
<LanguageDistributionContainer
alignTicks={true}
distribution={secondaryMeasure.value}
width={260}
/>
</div>
)}

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js View File

@@ -57,7 +57,7 @@ export default class MetaSize extends React.PureComponent {

return languageDistribution ? (
<div id="overview-language-distribution" className="overview-meta-size-lang-dist">
<LanguageDistributionContainer distribution={languageDistribution.value} />
<LanguageDistributionContainer distribution={languageDistribution.value} width={160} />
</div>
) : null;
};

+ 2
- 2
server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx View File

@@ -60,8 +60,8 @@ export default function Summary({ component, measures }: Props) {
</ul>

{nclocDistribution && (
<div className="huge-spacer-top" style={{ width: 260 }}>
<LanguageDistributionContainer distribution={nclocDistribution} />
<div className="huge-spacer-top">
<LanguageDistributionContainer distribution={nclocDistribution} width={260} />
</div>
)}
</section>

+ 1
- 5
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap View File

@@ -82,14 +82,10 @@ exports[`renders 1`] = `
</ul>
<div
className="huge-spacer-top"
style={
Object {
"width": 260,
}
}
>
<Connect(LanguageDistribution)
distribution="java=13;js=17"
width={260}
/>
</div>
</section>

+ 146
- 0
server/sonar-web/src/main/js/components/charts/Histogram.tsx View File

@@ -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<number, number>;
type YScale = ScaleBand<number>;

export default class Histogram extends React.PureComponent<Props> {
wrapWithTooltip(element: React.ReactNode, index: number) {
const tooltip = this.props.yTooltips && this.props.yTooltips[index];
return tooltip ? (
<Tooltip key={index} overlay={tooltip} placement="top">
{element}
</Tooltip>
) : (
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 <rect className="bar-chart-bar" x={x} y={y} width={width} height={BAR_HEIGHT} />;
}

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(
<text className="bar-chart-tick histogram-value" x={x} y={y} dx="1em" dy="0.3em">
{value}
</text>,
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 (
<text
className={'bar-chart-tick ' + historyTickClass}
x={x}
y={y}
dx={alignTicks ? 0 : '-1em'}
dy="0.3em">
{tick}
</text>
);
}

renderBars(xScale: XScale, yScale: YScale) {
return (
<g>
{this.props.bars.map((d, index) => {
return (
<g key={index}>
{this.renderBar(d, index, xScale, yScale)}
{this.renderValue(d, index, xScale, yScale)}
{this.renderTick(index, xScale, yScale)}
</g>
);
})}
</g>
);
}

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<number>()
.domain(bars.map((_, index) => index))
.rangeRound([0, availableHeight]);

return (
<svg className="bar-chart" width={this.props.width} height={this.props.height}>
<g transform={`translate(${this.props.alignTicks ? 4 : padding[3]}, ${padding[0]})`}>
{this.renderBars(xScale, yScale)}
</g>
</svg>
);
}
}

+ 21
- 14
server/sonar-web/src/main/js/components/charts/LanguageDistribution.tsx View File

@@ -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 (
<Histogram
alignTicks={props.alignTicks}
data={data}
bars={data}
height={distribution.length * 25}
padding={[0, 60, 0, 80]}
yTicks={yTicks}
yTooltips={yTooltips}
yValues={yValues}
barsWidth={10}
height={data.length * 25}
padding={[0, 60, 0, 80]}
width={props.width}
/>
);

function getLanguageName(langKey: string) {
if (langKey === '<null>') {
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;
}

+ 68
- 0
server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx View File

@@ -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(<Histogram bars={[100, 75, 150]} height={75} width={100} />)).toMatchSnapshot();
});

it('renders with yValues', () => {
expect(
shallow(
<Histogram
bars={[100, 75, 150]}
height={75}
yValues={['100.0', '75.0', '150.0']}
width={100}
/>
)
).toMatchSnapshot();
});

it('renders with yValues and yTicks', () => {
expect(
shallow(
<Histogram
bars={[100, 75, 150]}
height={75}
yTicks={['a', 'b', 'c']}
yValues={['100.0', '75.0', '150.0']}
width={100}
/>
)
).toMatchSnapshot();
});

it('renders with yValues, yTicks and yTooltips', () => {
expect(
shallow(
<Histogram
bars={[100, 75, 150]}
height={75}
yTicks={['a', 'b', 'c']}
yTooltips={['a - 100', 'b - 75', 'c - 150']}
yValues={['100.0', '75.0', '150.0']}
width={100}
/>
)
).toMatchSnapshot();
});

+ 34
- 0
server/sonar-web/src/main/js/components/charts/__tests__/LanguageDistribution-test.tsx View File

@@ -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(
<LanguageDistribution
distribution="java=1734;js=845;cpp=73;<null>=15"
languages={[{ key: 'java', name: 'Java' }, { key: 'js', name: 'JavaScript' }]}
width={100}
/>
)
).toMatchSnapshot();
});

+ 319
- 0
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap View File

@@ -0,0 +1,319 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<svg
className="bar-chart"
height={75}
width={100}
>
<g
transform="translate(10, 10)"
>
<g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={54}
x={0}
y={10}
/>
</g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={41}
x={0}
y={28}
/>
</g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={81}
x={0}
y={46}
/>
</g>
</g>
</g>
</svg>
`;

exports[`renders with yValues 1`] = `
<svg
className="bar-chart"
height={75}
width={100}
>
<g
transform="translate(10, 10)"
>
<g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={54}
x={0}
y={10}
/>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={53.33333333333333}
y={15}
>
100.0
</text>
</g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={41}
x={0}
y={28}
/>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={40}
y={33}
>
75.0
</text>
</g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={81}
x={0}
y={46}
/>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={80}
y={51}
>
150.0
</text>
</g>
</g>
</g>
</svg>
`;

exports[`renders with yValues and yTicks 1`] = `
<svg
className="bar-chart"
height={75}
width={100}
>
<g
transform="translate(10, 10)"
>
<g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={54}
x={0}
y={10}
/>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={53.33333333333333}
y={15}
>
100.0
</text>
<text
className="bar-chart-tick histogram-tick"
dx="-1em"
dy="0.3em"
x={0}
y={15}
>
a
</text>
</g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={41}
x={0}
y={28}
/>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={40}
y={33}
>
75.0
</text>
<text
className="bar-chart-tick histogram-tick"
dx="-1em"
dy="0.3em"
x={0}
y={33}
>
b
</text>
</g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={81}
x={0}
y={46}
/>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={80}
y={51}
>
150.0
</text>
<text
className="bar-chart-tick histogram-tick"
dx="-1em"
dy="0.3em"
x={0}
y={51}
>
c
</text>
</g>
</g>
</g>
</svg>
`;

exports[`renders with yValues, yTicks and yTooltips 1`] = `
<svg
className="bar-chart"
height={75}
width={100}
>
<g
transform="translate(10, 10)"
>
<g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={54}
x={0}
y={10}
/>
<Tooltip
overlay="a - 100"
placement="top"
>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={53.33333333333333}
y={15}
>
100.0
</text>
</Tooltip>
<text
className="bar-chart-tick histogram-tick"
dx="-1em"
dy="0.3em"
x={0}
y={15}
>
a
</text>
</g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={41}
x={0}
y={28}
/>
<Tooltip
overlay="b - 75"
placement="top"
>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={40}
y={33}
>
75.0
</text>
</Tooltip>
<text
className="bar-chart-tick histogram-tick"
dx="-1em"
dy="0.3em"
x={0}
y={33}
>
b
</text>
</g>
<g>
<rect
className="bar-chart-bar"
height={10}
width={81}
x={0}
y={46}
/>
<Tooltip
overlay="c - 150"
placement="top"
>
<text
className="bar-chart-tick histogram-value"
dx="1em"
dy="0.3em"
x={80}
y={51}
>
150.0
</text>
</Tooltip>
<text
className="bar-chart-tick histogram-tick"
dx="-1em"
dy="0.3em"
x={0}
y={51}
>
c
</text>
</g>
</g>
</g>
</svg>
`;

+ 48
- 0
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap View File

@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<Histogram
bars={
Array [
1734,
845,
73,
15,
]
}
height={100}
padding={
Array [
0,
60,
0,
80,
]
}
width={100}
yTicks={
Array [
"Java",
"JavaScript",
"cpp",
"unknown",
]
}
yTooltips={
Array [
"1,734",
"",
"",
"",
]
}
yValues={
Array [
"1.7k",
"845",
"73",
"15",
]
}
/>
`;

+ 0
- 166
server/sonar-web/src/main/js/components/charts/histogram.js View File

@@ -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 (
<text
key={index}
className={'bar-chart-tick ' + historyTickClass}
onClick={this.props.onBarClick && this.handleClick.bind(this, point)}
style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
data-title={tooltip}
data-toggle={tooltip ? 'tooltip' : null}
x={x}
y={y}
dx={this.props.alignTicks ? 0 : '-1em'}
dy="0.3em">
{label}
</text>
);
});
return <g>{ticks}</g>;
},

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 (
<text
key={index}
onClick={this.props.onBarClick && this.handleClick.bind(this, point)}
className="bar-chart-tick histogram-value"
style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
x={x}
y={y}
dx="1em"
dy="0.3em">
{value}
</text>
);
});
return <g>{ticks}</g>;
},

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 (
<rect
key={index}
className="bar-chart-bar"
onClick={this.props.onBarClick && this.handleClick.bind(this, d)}
style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
x={x}
y={y}
width={width}
height={this.props.barsHeight}
/>
);
});
return <g>{bars}</g>;
},

render() {
if (!this.state.width || !this.state.height) {
return <div />;
}

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 (
<svg className="bar-chart" width={this.state.width} height={this.state.height}>
<g
transform={`translate(${this.props.alignTicks ? 4 : this.props.padding[3]}, ${this.props
.padding[0]})`}>
{this.renderTicks(xScale, yScale)}
{this.renderValues(xScale, yScale)}
{this.renderBars(xScale, yScale)}
</g>
</svg>
);
}
});

+ 14
- 0
server/sonar-web/yarn.lock View File

@@ -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"

Loading…
Cancel
Save