aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorPascal Mugnier <pascal.mugnier@sonarsource.com>2018-04-11 15:45:43 +0200
committerSonarTech <sonartech@sonarsource.com>2018-04-11 20:20:48 +0200
commitad9ecec15f216c12f7e6456fc9d3cde0007b860f (patch)
tree0d1d49feacc093f4773301ffa95da07e2fbdb211 /server/sonar-web
parentc978976137ae9e4cc7197a75ae24708e1c027d80 (diff)
downloadsonarqube-ad9ecec15f216c12f7e6456fc9d3cde0007b860f.tar.gz
sonarqube-ad9ecec15f216c12f7e6456fc9d3cde0007b860f.zip
SONAR-9416 Enable zoom on bubble charts (#127)
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/package.json1
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/badges.css6
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/graphics.css17
-rw-r--r--server/sonar-web/src/main/js/components/charts/BubbleChart.d.ts48
-rw-r--r--server/sonar-web/src/main/js/components/charts/BubbleChart.js270
-rw-r--r--server/sonar-web/src/main/js/components/charts/BubbleChart.tsx434
-rw-r--r--server/sonar-web/yarn.lock9
7 files changed, 465 insertions, 320 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index eadf694d63a..78e0324d5a3 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -55,6 +55,7 @@
"@types/react-redux": "5.0.12",
"@types/react-router": "3.0.13",
"@types/react-select": "1.0.59",
+ "@types/react-virtualized": "9.7.15",
"autoprefixer": "7.1.6",
"babel-core": "6.26.0",
"babel-jest": "22.0.6",
diff --git a/server/sonar-web/src/main/js/app/styles/components/badges.css b/server/sonar-web/src/main/js/app/styles/components/badges.css
index 602f7513f1a..1dd87fe7d64 100644
--- a/server/sonar-web/src/main/js/app/styles/components/badges.css
+++ b/server/sonar-web/src/main/js/app/styles/components/badges.css
@@ -144,3 +144,9 @@ a.badge-focus:active {
font-size: var(--smallFontSize);
font-weight: 400;
}
+
+.outline-badge.active {
+ color: var(--baseFontColor);
+ border: 1px solid var(--blue);
+ background-color: var(--lightBlue);
+}
diff --git a/server/sonar-web/src/main/js/app/styles/components/graphics.css b/server/sonar-web/src/main/js/app/styles/components/graphics.css
index 049dde27ec5..03128aa2a68 100644
--- a/server/sonar-web/src/main/js/app/styles/components/graphics.css
+++ b/server/sonar-web/src/main/js/app/styles/components/graphics.css
@@ -199,12 +199,20 @@
* Bubble Chart
*/
+.bubble-chart.is-moving {
+ cursor: move;
+}
+
+.bubble-chart text {
+ user-select: none;
+}
+
.bubble-chart-bubble {
fill: var(--blue);
fill-opacity: 0.2;
stroke: var(--blue);
cursor: pointer;
- transition: all 0.2s ease;
+ transition: fill-opacity 0.2s ease;
}
.bubble-chart-bubble:hover {
@@ -226,6 +234,13 @@
text-anchor: end;
}
+.bubble-chart-zoom {
+ position: absolute;
+ right: 20px;
+ top: 20px;
+ z-index: var(--aboveNormalZIndex);
+}
+
/*
* Legends
*/
diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.d.ts b/server/sonar-web/src/main/js/components/charts/BubbleChart.d.ts
deleted file mode 100644
index f428115c4b3..00000000000
--- a/server/sonar-web/src/main/js/components/charts/BubbleChart.d.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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';
-
-interface Item {
- x: number;
- y: number;
- size: number;
- color?: string;
- key?: string;
- link?: any;
- tooltip?: React.ReactNode;
-}
-
-interface Props {
- items: Item[];
- sizeRange?: [number, number];
- displayXGrid?: boolean;
- displayXTicks?: boolean;
- displayYGrid?: boolean;
- displayYTicks?: boolean;
- height: number;
- padding: [number, number, number, number];
- formatXTick: (tick: number) => string;
- formatYTick: (tick: number) => string;
- onBubbleClick?: (link?: any) => void;
- xDomain?: [number, number];
- yDomain?: [number, number];
-}
-
-export default class BubbleChart extends React.Component<Props> {}
diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.js b/server/sonar-web/src/main/js/components/charts/BubbleChart.js
deleted file mode 100644
index 73fcce09b78..00000000000
--- a/server/sonar-web/src/main/js/components/charts/BubbleChart.js
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import React from 'react';
-import { Link } from 'react-router';
-import { min, max } from 'd3-array';
-import { scaleLinear } from 'd3-scale';
-import { sortBy, uniq } from 'lodash';
-import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
-import Tooltip from '../controls/Tooltip';
-
-/*::
-type Scale = {
- (number): number,
- range: () => [number, number],
- ticks: number => Array<number>
-};
-*/
-
-const TICKS_COUNT = 5;
-
-export class Bubble extends React.PureComponent {
- /*:: props: {
- color?: string,
- link?: string,
- onClick: (?string) => void,
- r: number,
- tooltip?: string | React$Element<*>,
- x: number,
- y: number
- };
-*/
-
- handleClick = () => {
- if (this.props.onClick) {
- this.props.onClick(this.props.link);
- }
- };
-
- render() {
- let circle = (
- <circle
- className="bubble-chart-bubble"
- onClick={this.props.onClick ? this.handleClick : undefined}
- r={this.props.r}
- style={{ fill: this.props.color, stroke: this.props.color }}
- transform={`translate(${this.props.x}, ${this.props.y})`}
- />
- );
-
- if (this.props.link && !this.props.onClick) {
- circle = <Link to={this.props.link}>{circle}</Link>;
- }
-
- return (
- <Tooltip overlay={this.props.tooltip || undefined}>
- <g>{circle}</g>
- </Tooltip>
- );
- }
-}
-
-export default class BubbleChart extends React.PureComponent {
- /*:: props: {|
- items: Array<{|
- x: number,
- y: number,
- size: number,
- color?: string,
- key?: string,
- link?: string,
- tooltip?: string | React$Element<*>
- |}>,
- sizeRange?: [number, number],
- displayXGrid: boolean,
- displayXTicks: boolean,
- displayYGrid: boolean,
- displayYTicks: boolean,
- height: number,
- padding: [number, number, number, number],
- formatXTick: number => string,
- formatYTick: number => string,
- onBubbleClick?: (?string) => void,
- xDomain?: [number, number],
- yDomain?: [number, number]
- |};
-*/
-
- static defaultProps = {
- sizeRange: [5, 45],
- displayXGrid: true,
- displayYGrid: true,
- displayXTicks: true,
- displayYTicks: true,
- padding: [10, 10, 10, 10],
- formatXTick: d => d,
- formatYTick: d => d
- };
-
- getXRange(xScale /*: Scale */, sizeScale /*: Scale */, availableWidth /*: number */) {
- const minX = min(this.props.items, d => xScale(d.x) - sizeScale(d.size));
- const maxX = max(this.props.items, d => xScale(d.x) + sizeScale(d.size));
- const dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0];
- const dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
- return [dMinX, availableWidth - dMaxX];
- }
-
- getYRange(yScale /*: Scale */, sizeScale /*: Scale */, availableHeight /*: number */) {
- const minY = min(this.props.items, d => yScale(d.y) - sizeScale(d.size));
- const maxY = max(this.props.items, d => yScale(d.y) + sizeScale(d.size));
- const dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1];
- const dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
- return [availableHeight - dMaxY, dMinY];
- }
-
- getTicks(scale /*: Scale */, format /*: number => string */) {
- const ticks = scale.ticks(TICKS_COUNT).map(tick => format(tick));
- const uniqueTicksCount = uniq(ticks).length;
- const ticksCount = uniqueTicksCount < TICKS_COUNT ? uniqueTicksCount - 1 : TICKS_COUNT;
- return scale.ticks(ticksCount);
- }
-
- renderXGrid(ticks /*: Array<number> */, xScale /*: Scale */, yScale /*: Scale */) {
- if (!this.props.displayXGrid) {
- return null;
- }
-
- const lines = ticks.map((tick, index) => {
- const x = xScale(tick);
- const y1 = yScale.range()[0];
- const y2 = yScale.range()[1];
- return <line key={index} x1={x} x2={x} y1={y1} y2={y2} className="bubble-chart-grid" />;
- });
-
- return <g ref="xGrid">{lines}</g>;
- }
-
- renderYGrid(ticks /*: Array<number> */, xScale /*: Scale */, yScale /*: Scale */) {
- if (!this.props.displayYGrid) {
- return null;
- }
-
- const lines = ticks.map((tick, index) => {
- const y = yScale(tick);
- const x1 = xScale.range()[0];
- const x2 = xScale.range()[1];
- return <line key={index} x1={x1} x2={x2} y1={y} y2={y} className="bubble-chart-grid" />;
- });
-
- return <g ref="yGrid">{lines}</g>;
- }
-
- renderXTicks(xTicks /*: Array<number> */, xScale /*: Scale */, yScale /*: Scale */) {
- if (!this.props.displayXTicks) {
- return null;
- }
-
- const ticks = xTicks.map((tick, index) => {
- const x = xScale(tick);
- const y = yScale.range()[0];
- const innerText = this.props.formatXTick(tick);
- return (
- <text key={index} className="bubble-chart-tick" x={x} y={y} dy="1.5em">
- {innerText}
- </text>
- );
- });
-
- return <g>{ticks}</g>;
- }
-
- renderYTicks(yTicks /*: Array<number> */, xScale /*: Scale */, yScale /*: Scale */) {
- if (!this.props.displayYTicks) {
- return null;
- }
-
- const ticks = yTicks.map((tick, index) => {
- const x = xScale.range()[0];
- const y = yScale(tick);
- const innerText = this.props.formatYTick(tick);
- return (
- <text
- key={index}
- className="bubble-chart-tick bubble-chart-tick-y"
- x={x}
- y={y}
- dx="-0.5em"
- dy="0.3em">
- {innerText}
- </text>
- );
- });
-
- return <g>{ticks}</g>;
- }
-
- renderChart(width /*: number */) {
- const availableWidth = width - this.props.padding[1] - this.props.padding[3];
- const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
-
- const xScale = scaleLinear()
- .domain(this.props.xDomain || [0, max(this.props.items, d => d.x)])
- .range([0, availableWidth])
- .nice();
- const yScale = scaleLinear()
- .domain(this.props.yDomain || [0, max(this.props.items, d => d.y)])
- .range([availableHeight, 0])
- .nice();
- const sizeScale = scaleLinear()
- .domain(this.props.sizeDomain || [0, max(this.props.items, d => d.size)])
- .range(this.props.sizeRange);
-
- const xScaleOriginal = xScale.copy();
- const yScaleOriginal = yScale.copy();
-
- xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
- yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
-
- const bubbles = sortBy(this.props.items, b => -b.size).map((item, index) => {
- return (
- <Bubble
- key={item.key || index}
- link={item.link}
- tooltip={item.tooltip}
- x={xScale(item.x)}
- y={yScale(item.y)}
- r={sizeScale(item.size)}
- color={item.color}
- onClick={this.props.onBubbleClick}
- />
- );
- });
-
- const xTicks = this.getTicks(xScale, this.props.formatXTick);
- const yTicks = this.getTicks(yScale, this.props.formatYTick);
-
- return (
- <svg className="bubble-chart" width={width} height={this.props.height}>
- <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
- {this.renderXGrid(xTicks, xScale, yScale)}
- {this.renderXTicks(xTicks, xScale, yScaleOriginal)}
- {this.renderYGrid(yTicks, xScale, yScale)}
- {this.renderYTicks(yTicks, xScaleOriginal, yScale)}
- {bubbles}
- </g>
- </svg>
- );
- }
-
- render() {
- return <AutoSizer disableHeight={true}>{size => this.renderChart(size.width)}</AutoSizer>;
- }
-}
diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx
new file mode 100644
index 00000000000..cd7033c2413
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx
@@ -0,0 +1,434 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
+import { Link } from 'react-router';
+import { min, max } from 'd3-array';
+import { scaleLinear, ScaleLinear } from 'd3-scale';
+import { sortBy, uniq } from 'lodash';
+import Tooltip from '../controls/Tooltip';
+import { translate } from '../../helpers/l10n';
+
+const TICKS_COUNT = 5;
+
+interface BubbleProps {
+ color?: string;
+ link?: string;
+ onClick?: (link?: string) => void;
+ r: number;
+ tooltip?: string | React.ReactNode;
+ x: number;
+ y: number;
+}
+
+export class Bubble extends React.PureComponent<BubbleProps> {
+ handleClick = (event: React.MouseEvent<SVGCircleElement>) => {
+ if (this.props.onClick) {
+ event.stopPropagation();
+ event.preventDefault();
+ this.props.onClick(this.props.link);
+ }
+ };
+
+ render() {
+ let circle = (
+ <circle
+ className="bubble-chart-bubble"
+ onClick={this.props.onClick ? this.handleClick : undefined}
+ r={this.props.r}
+ style={{ fill: this.props.color, stroke: this.props.color }}
+ transform={`translate(${this.props.x}, ${this.props.y})`}
+ />
+ );
+
+ if (this.props.link && !this.props.onClick) {
+ circle = <Link to={this.props.link}>{circle}</Link>;
+ }
+
+ return (
+ <Tooltip overlay={this.props.tooltip || undefined}>
+ <g>{circle}</g>
+ </Tooltip>
+ );
+ }
+}
+
+interface Item {
+ color?: string;
+ key?: string;
+ link?: any;
+ size: number;
+ tooltip?: React.ReactNode;
+ x: number;
+ y: number;
+}
+
+interface Props {
+ displayXGrid?: boolean;
+ displayXTicks?: boolean;
+ displayYGrid?: boolean;
+ displayYTicks?: boolean;
+ formatXTick: (tick: number) => string;
+ formatYTick: (tick: number) => string;
+ height: number;
+ items: Item[];
+ onBubbleClick?: (link?: string) => void;
+ padding: [number, number, number, number];
+ sizeDomain?: [number, number];
+ sizeRange?: [number, number];
+ xDomain?: [number, number];
+ yDomain?: [number, number];
+}
+
+interface State {
+ isMoving: boolean;
+ moveOrigin: { x: number; y: number };
+ zoom: number;
+ zoomOrigin: { x: number; y: number };
+}
+
+type Scale = ScaleLinear<number, number>;
+
+export default class BubbleChart extends React.Component<Props, State> {
+ node: SVGSVGElement | null = null;
+
+ static defaultProps = {
+ displayXGrid: true,
+ displayXTicks: true,
+ displayYGrid: true,
+ displayYTicks: true,
+ formatXTick: (d: number) => d,
+ formatYTick: (d: number) => d,
+ padding: [10, 10, 10, 10],
+ sizeRange: [5, 45]
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ isMoving: false,
+ moveOrigin: { x: 0, y: 0 },
+ zoom: 1,
+ zoomOrigin: { x: 0, y: 0 }
+ };
+ }
+
+ componentDidMount() {
+ document.addEventListener('mouseup', this.stopMoving);
+ document.addEventListener('mousemove', this.updateZoomCenter);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mouseup', this.stopMoving);
+ document.removeEventListener('mousemove', this.updateZoomCenter);
+ }
+
+ startMoving = (event: React.MouseEvent<SVGSVGElement>) => {
+ if (this.node && this.state.zoom > 1) {
+ const rect = this.node.getBoundingClientRect();
+ this.setState({
+ isMoving: true,
+ moveOrigin: { x: event.clientX - rect.left, y: event.clientY - rect.top }
+ });
+ }
+ };
+
+ updateZoomCenter = (event: MouseEvent) => {
+ if (this.node && this.state.isMoving) {
+ const rect = this.node.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+ this.setState(state => ({
+ zoomOrigin: {
+ x: Math.max(-100, state.zoomOrigin.x + (state.moveOrigin.x - x) / state.zoom),
+ y: Math.max(-100, state.zoomOrigin.y + (state.moveOrigin.y - y) / state.zoom)
+ },
+ moveOrigin: { x, y }
+ }));
+ }
+ };
+
+ stopMoving = () => {
+ this.setState({ isMoving: false });
+ };
+
+ onWheel = (event: React.WheelEvent<SVGSVGElement>) => {
+ if (this.node) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const rect = this.node.getBoundingClientRect();
+ const mouseX = event.clientX - rect.left - this.props.padding[1];
+ const mouseY = event.clientY - rect.top - this.props.padding[0];
+
+ let delta = event.deltaY;
+ if ((event as any).webkitDirectionInvertedFromDevice) {
+ delta = -delta;
+ }
+
+ if (delta < 0) {
+ this.handleZoomOut(mouseX, mouseY);
+ } else {
+ this.handleZoomIn(mouseX, mouseY);
+ }
+ }
+ };
+
+ handleZoomOut = (x: number, y: number) => {
+ if (this.state.zoom === 1) {
+ this.setState(state => ({
+ zoom: Math.max(1.0, state.zoom - 0.5),
+ zoomOrigin: { x, y }
+ }));
+ } else {
+ this.setState(state => ({
+ zoom: Math.max(1.0, state.zoom - 0.5)
+ }));
+ }
+ };
+
+ handleZoomIn = (x: number, y: number) => {
+ if (this.state.zoom === 1) {
+ this.setState(state => ({
+ zoom: Math.min(10.0, state.zoom + 0.5),
+ zoomOrigin: { x, y }
+ }));
+ } else {
+ this.setState(state => ({
+ zoom: Math.min(10.0, state.zoom + 0.5)
+ }));
+ }
+ };
+
+ resetZoom = () => {
+ this.setState({
+ zoom: 1,
+ zoomOrigin: { x: 0, y: 0 }
+ });
+ };
+
+ getXRange(xScale: Scale, sizeScale: Scale, availableWidth: number) {
+ const minX = min(this.props.items, d => xScale(d.x) - sizeScale(d.size)) || 0;
+ const maxX = max(this.props.items, d => xScale(d.x) + sizeScale(d.size)) || 0;
+ const dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0];
+ const dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
+ return [dMinX, availableWidth - dMaxX];
+ }
+
+ getYRange(yScale: Scale, sizeScale: Scale, availableHeight: number) {
+ const minY = min(this.props.items, d => yScale(d.y) - sizeScale(d.size)) || 0;
+ const maxY = max(this.props.items, d => yScale(d.y) + sizeScale(d.size)) || 0;
+ const dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1];
+ const dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
+ return [availableHeight - dMaxY, dMinY];
+ }
+
+ getTicks(scale: Scale, format: (d: number) => string) {
+ const ticks = scale.ticks(TICKS_COUNT).map(tick => format(tick));
+ const uniqueTicksCount = uniq(ticks).length;
+ const ticksCount = uniqueTicksCount < TICKS_COUNT ? uniqueTicksCount - 1 : TICKS_COUNT;
+ return scale.ticks(ticksCount);
+ }
+
+ getZoomLevelLabel = () => this.state.zoom * 100 + '%';
+
+ renderXGrid = (ticks: Array<number>, xScale: Scale, yScale: Scale, delta: number) => {
+ if (!this.props.displayXGrid) {
+ return null;
+ }
+
+ const lines = ticks.map((tick, index) => {
+ const x = xScale(tick);
+ const y1 = yScale.range()[0];
+ const y2 = yScale.range()[1];
+ return (
+ <line
+ className="bubble-chart-grid"
+ key={index}
+ x1={x * this.state.zoom - delta}
+ x2={x * this.state.zoom - delta}
+ y1={y1}
+ y2={y2}
+ />
+ );
+ });
+
+ return <g>{lines}</g>;
+ };
+
+ renderYGrid = (ticks: Array<number>, xScale: Scale, yScale: Scale, delta: number) => {
+ if (!this.props.displayYGrid) {
+ return null;
+ }
+
+ const lines = ticks.map((tick, index) => {
+ const y = yScale(tick);
+ const x1 = xScale.range()[0];
+ const x2 = xScale.range()[1];
+ return (
+ <line
+ className="bubble-chart-grid"
+ key={index}
+ x1={x1}
+ x2={x2}
+ y1={y * this.state.zoom - delta}
+ y2={y * this.state.zoom - delta}
+ />
+ );
+ });
+
+ return <g>{lines}</g>;
+ };
+
+ renderXTicks = (xTicks: Array<number>, xScale: Scale, yScale: Scale, delta: number) => {
+ if (!this.props.displayXTicks) {
+ return null;
+ }
+
+ const ticks = xTicks.map((tick, index) => {
+ const x = xScale(tick);
+ const y = yScale.range()[0];
+ const innerText = this.props.formatXTick(tick);
+ return (
+ <text
+ className="bubble-chart-tick"
+ dy="1.5em"
+ key={index}
+ x={x * this.state.zoom - delta}
+ y={y}>
+ {innerText}
+ </text>
+ );
+ });
+
+ return <g>{ticks}</g>;
+ };
+
+ renderYTicks = (yTicks: Array<number>, xScale: Scale, yScale: Scale, delta: number) => {
+ if (!this.props.displayYTicks) {
+ return null;
+ }
+
+ const ticks = yTicks.map((tick, index) => {
+ const x = xScale.range()[0];
+ const y = yScale(tick);
+ const innerText = this.props.formatYTick(tick);
+ return (
+ <text
+ className="bubble-chart-tick bubble-chart-tick-y"
+ dx="-0.5em"
+ dy="0.3em"
+ key={index}
+ x={x}
+ y={y * this.state.zoom - delta}>
+ {innerText}
+ </text>
+ );
+ });
+
+ return <g>{ticks}</g>;
+ };
+
+ renderChart = (width: number) => {
+ const availableWidth = width - this.props.padding[1] - this.props.padding[3];
+ const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
+
+ const xScale = scaleLinear()
+ .domain(this.props.xDomain || [0, max(this.props.items, d => d.x) || 0])
+ .range([0, availableWidth])
+ .nice();
+ const yScale = scaleLinear()
+ .domain(this.props.yDomain || [0, max(this.props.items, d => d.y) || 0])
+ .range([availableHeight, 0])
+ .nice();
+ const sizeScale = scaleLinear()
+ .domain(this.props.sizeDomain || [0, max(this.props.items, d => d.size) || 0])
+ .range(this.props.sizeRange || []);
+
+ const xScaleOriginal = xScale.copy();
+ const yScaleOriginal = yScale.copy();
+
+ xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
+ yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
+
+ const centerXDelta = this.state.zoomOrigin.x * this.state.zoom - this.state.zoomOrigin.x;
+ const centerYDelta = this.state.zoomOrigin.y * this.state.zoom - this.state.zoomOrigin.y;
+
+ const bubbles = sortBy(this.props.items, b => -b.size).map((item, index) => {
+ return (
+ <Bubble
+ color={item.color}
+ key={item.key || index}
+ link={item.link}
+ onClick={this.props.onBubbleClick}
+ r={sizeScale(item.size)}
+ tooltip={item.tooltip}
+ x={xScale(item.x) * this.state.zoom - centerXDelta}
+ y={yScale(item.y) * this.state.zoom - centerYDelta}
+ />
+ );
+ });
+
+ const xTicks = this.getTicks(xScale, this.props.formatXTick);
+ const yTicks = this.getTicks(yScale, this.props.formatYTick);
+
+ return (
+ <svg
+ className={classNames('bubble-chart', { 'is-moving': this.state.isMoving })}
+ height={this.props.height}
+ onMouseDown={this.startMoving}
+ onWheel={this.onWheel}
+ ref={node => (this.node = node)}
+ width={width}>
+ <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+ <svg
+ height={this.props.height - this.props.padding[0] - this.props.padding[2]}
+ style={{ overflow: 'hidden' }}
+ width={width}>
+ {this.renderXGrid(xTicks, xScale, yScale, centerXDelta)}
+ {this.renderYGrid(yTicks, xScale, yScale, centerYDelta)}
+ {bubbles}
+ </svg>
+ {this.renderXTicks(xTicks, xScale, yScaleOriginal, centerXDelta)}
+ {this.renderYTicks(yTicks, xScaleOriginal, yScale, centerYDelta)}
+ </g>
+ </svg>
+ );
+ };
+
+ render() {
+ return (
+ <div>
+ <div className="bubble-chart-zoom">
+ <Tooltip overlay={translate('component_measures.bubble_chart.zoom_level')}>
+ <Link
+ className={classNames('outline-badge', { active: this.state.zoom > 1 })}
+ onClick={this.resetZoom}
+ to="#">
+ {this.getZoomLevelLabel()}
+ </Link>
+ </Tooltip>
+ </div>
+ <AutoSizer disableHeight={true}>{size => this.renderChart(size.width)}</AutoSizer>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index dd0142459a6..ad4e577368c 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -63,7 +63,7 @@
version "6.0.90"
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.90.tgz#0ed74833fa1b73dcdb9409dcb1c97ec0a8b13b02"
-"@types/prop-types@15.5.2":
+"@types/prop-types@*", "@types/prop-types@15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.2.tgz#3c6b8dceb2906cc87fe4358e809f9d20c8d59be1"
@@ -110,6 +110,13 @@
dependencies:
"@types/react" "*"
+"@types/react-virtualized@9.7.15":
+ version "9.7.15"
+ resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.7.15.tgz#223a30403317e9955d836a3c4f0561087b43617e"
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/react" "*"
+
"@types/react@*", "@types/react@16.0.29":
version "16.0.29"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.29.tgz#4eea6a8de9f40ca71d580ae7a9f3b4b77b368de8"