/* * 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 _ from 'underscore'; import d3 from 'd3'; import React from 'react'; import { TreemapBreadcrumbs } from './treemap-breadcrumbs'; import { ResizeMixin } from './../mixins/resize-mixin'; import { TooltipsMixin } from './../mixins/tooltips-mixin'; import { translate } from '../../helpers/l10n'; const SIZE_SCALE = d3.scale.linear() .domain([3, 15]) .range([11, 18]) .clamp(true); function mostCommitPrefix (strings) { const sortedStrings = strings.slice(0).sort(); const firstString = sortedStrings[0]; const firstStringLength = firstString.length; const lastString = sortedStrings[sortedStrings.length - 1]; let i = 0; while (i < firstStringLength && firstString.charAt(i) === lastString.charAt(i)) { i++; } const prefix = firstString.substr(0, i); const lastPrefixPart = _.last(prefix.split(/[\s\\\/]/)); return prefix.substr(0, prefix.length - lastPrefixPart.length); } 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, onClick: React.PropTypes.func }, renderLink() { if (!this.props.link) { return null; } if (this.props.width < 24 || this.props.height < 24) { return null; } return e.stopPropagation()} className="treemap-link" href={this.props.link} style={{ fontSize: 12 }}> ; }, render () { let tooltipAttrs = {}; if (this.props.tooltip) { tooltipAttrs = { 'data-toggle': 'tooltip', 'data-title': this.props.tooltip }; } let cellStyles = { left: this.props.x, top: this.props.y, width: this.props.width, height: this.props.height, backgroundColor: this.props.fill, fontSize: SIZE_SCALE(this.props.width / this.props.label.length), lineHeight: `${this.props.height}px`, cursor: typeof this.props.onClick === 'function' ? 'pointer' : 'default' }; let isTextVisible = this.props.width >= 40 && this.props.height >= 40; return
{this.renderLink()}
; } }); export const Treemap = React.createClass({ propTypes: { items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, height: React.PropTypes.number, onRectangleClick: React.PropTypes.func }, mixins: [ResizeMixin, TooltipsMixin], getInitialState() { return { width: this.props.width, height: this.props.height }; }, renderWhenNoData () { return
{translate('no_data')}
; }, render () { if (!this.state.width || !this.state.height) { return
 
; } if (!this.props.items.length) { return this.renderWhenNoData(); } let treemap = d3.layout.treemap() .round(true) .value(d => d.size) .sort((a, b) => a.value - b.value) .size([this.state.width, this.state.height]); let nodes = treemap .nodes({ children: this.props.items }) .filter(d => !d.children) .filter(d => !!d.dx && !!d.dy); let prefix = mostCommitPrefix(this.props.items.map(item => item.label)); let prefixLength = prefix.length; let rectangles = nodes.map(node => { const key = node.label; let label = prefixLength ? `${prefix}
${node.label.substr(prefixLength)}` : node.label; const onClick = this.props.canBeClicked(node) ? () => this.props.onRectangleClick(node) : null; return ; }); return
{rectangles}
; } });