diff options
author | Pascal Mugnier <pascal.mugnier@sonarsource.com> | 2018-04-11 15:45:43 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-04-11 20:20:48 +0200 |
commit | ad9ecec15f216c12f7e6456fc9d3cde0007b860f (patch) | |
tree | 0d1d49feacc093f4773301ffa95da07e2fbdb211 /server/sonar-web | |
parent | c978976137ae9e4cc7197a75ae24708e1c027d80 (diff) | |
download | sonarqube-ad9ecec15f216c12f7e6456fc9d3cde0007b860f.tar.gz sonarqube-ad9ecec15f216c12f7e6456fc9d3cde0007b860f.zip |
SONAR-9416 Enable zoom on bubble charts (#127)
Diffstat (limited to 'server/sonar-web')
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" |