@@ -23,6 +23,8 @@ import { AutoSizer } from 'react-virtualized'; | |||
import { scaleLinear, scaleOrdinal } from 'd3-scale'; | |||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | |||
import TreeMap from '../../../components/charts/TreeMap'; | |||
import ColorBoxLegend from '../../../components/charts/ColorBoxLegend'; | |||
import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'; | |||
import { translate, translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | |||
import { getComponentUrl } from '../../../helpers/urls'; | |||
@@ -41,6 +43,8 @@ type State = { | |||
}; | |||
const HEIGHT = 500; | |||
const COLORS = ['#00aa00', '#b0d513', '#eabe06', '#ed7d20', '#d4333f']; | |||
const LEVEL_COLORS = ['#d4333f', '#ed7d20', '#00aa00', '#b4b4b4']; | |||
export default class TreeMapView extends React.PureComponent { | |||
props: Props; | |||
@@ -52,7 +56,7 @@ export default class TreeMapView extends React.PureComponent { | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (nextProps.components !== this.props.components) { | |||
if (nextProps.components !== this.props.components || nextProps.metric !== this.props.metric) { | |||
this.setState({ treemapItems: this.getTreemapComponents(nextProps) }); | |||
} | |||
} | |||
@@ -95,24 +99,15 @@ export default class TreeMapView extends React.PureComponent { | |||
}; | |||
getLevelColorScale = () => | |||
scaleOrdinal() | |||
.domain(['ERROR', 'WARN', 'OK', 'NONE']) | |||
.range(['#d4333f', '#ed7d20', '#00aa00', '#b4b4b4']); | |||
scaleOrdinal().domain(['ERROR', 'WARN', 'OK', 'NONE']).range(LEVEL_COLORS); | |||
getPercentColorScale = (metric: Metric) => { | |||
const color = scaleLinear().domain([0, 25, 50, 75, 100]); | |||
color.range( | |||
metric.direction === 1 | |||
? ['#d4333f', '#ed7d20', '#eabe06', '#b0d513', '#00aa00'] | |||
: ['#00aa00', '#b0d513', '#eabe06', '#ed7d20', '#d4333f'] | |||
); | |||
color.range(metric.direction === 1 ? COLORS.reverse() : COLORS); | |||
return color; | |||
}; | |||
getRatingColorScale = () => | |||
scaleLinear() | |||
.domain([1, 2, 3, 4, 5]) | |||
.range(['#00aa00', '#b0d513', '#eabe06', '#ed7d20', '#d4333f']); | |||
getRatingColorScale = () => scaleLinear().domain([1, 2, 3, 4, 5]).range(COLORS); | |||
getColorScale = (metric: Metric) => { | |||
if (metric.type === 'LEVEL') { | |||
@@ -144,6 +139,30 @@ export default class TreeMapView extends React.PureComponent { | |||
); | |||
}; | |||
renderLegend() { | |||
const { metric } = this.props; | |||
const colorScale = this.getColorScale(metric); | |||
if (['LEVEL', 'RATING'].includes(metric.type)) { | |||
return ( | |||
<ColorBoxLegend | |||
className="measure-details-treemap-legend color-box-full" | |||
colorScale={colorScale} | |||
metricType={metric.type} | |||
/> | |||
); | |||
} | |||
return ( | |||
<ColorGradientLegend | |||
className="measure-details-treemap-legend" | |||
colorScale={colorScale} | |||
colorNA="#777" | |||
direction={metric.direction} | |||
height={20} | |||
width={200} | |||
/> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<div className="measure-details-treemap"> | |||
@@ -160,6 +179,9 @@ export default class TreeMapView extends React.PureComponent { | |||
translate('metric.ncloc.name') | |||
)} | |||
</li> | |||
<li className="pull-right"> | |||
{this.renderLegend()} | |||
</li> | |||
</ul> | |||
<AutoSizer> | |||
{({ width }) => |
@@ -71,6 +71,14 @@ | |||
font-size: 12px; | |||
} | |||
.measure-details-treemap-legend { | |||
margin-right: -4px; | |||
} | |||
.measure-details-treemap-legend.color-box-legend { | |||
margin-right: 0; | |||
} | |||
.measure-view-select { | |||
width: 50px; | |||
} |
@@ -0,0 +1,57 @@ | |||
/* | |||
* 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
type Props = { | |||
className?: string, | |||
colorScale: Object, | |||
colorNA?: string, | |||
metricType: string | |||
}; | |||
export default function ColorBoxLegend({ className, colorScale, colorNA, metricType }: Props) { | |||
const colorDomain = colorScale.domain(); | |||
const colorRange = colorScale.range(); | |||
return ( | |||
<div className={classNames('color-box-legend', className)}> | |||
{colorDomain.map((value, idx) => | |||
<div key={value}> | |||
<span className="color-box-legend-rect" style={{ borderColor: colorRange[idx] }}> | |||
<span | |||
className="color-box-legend-rect-inner" | |||
style={{ backgroundColor: colorRange[idx] }} | |||
/> | |||
</span> | |||
{formatMeasure(value, metricType)} | |||
</div> | |||
)} | |||
{colorNA && | |||
<div> | |||
<span className="color-box-legend-rect" style={{ borderColor: colorNA }}> | |||
<span className="color-box-legend-rect-inner" style={{ backgroundColor: colorNA }} /> | |||
</span> | |||
N/A | |||
</div>} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,96 @@ | |||
/* | |||
* 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
type Props = { | |||
className?: string, | |||
colorScale: Object, | |||
colorNA?: string, | |||
direction?: number, | |||
padding?: Array<number>, | |||
height: number, | |||
width: number | |||
}; | |||
const NA_SPACING = 4; | |||
export default function ColorGradientLegend({ | |||
className, | |||
colorScale, | |||
colorNA, | |||
direction, | |||
padding = [12, 24, 0, 0], | |||
height, | |||
width | |||
}: Props) { | |||
let colorDomain = colorScale.domain(); | |||
let colorRange = colorScale.range(); | |||
if (direction !== 1) { | |||
colorDomain = colorDomain.reverse(); | |||
colorRange = colorRange.reverse(); | |||
} | |||
const lastColorIdx = colorRange.length - 1; | |||
const lastDomainIdx = colorDomain.length - 1; | |||
const widthNoPadding = width - padding[1]; | |||
const rectHeight = height - padding[0]; | |||
return ( | |||
<svg className={className} width={width} height={height}> | |||
<defs> | |||
<linearGradient id="gradient-legend"> | |||
{colorRange.map((color, idx) => | |||
<stop key={idx} offset={idx / lastColorIdx} stopColor={color} /> | |||
)} | |||
</linearGradient> | |||
</defs> | |||
<g transform={`translate(${padding[3]}, ${padding[0]})`}> | |||
<rect fill="url(#gradient-legend)" x={0} y={0} height={rectHeight} width={widthNoPadding} /> | |||
{colorDomain.map((d, idx) => | |||
<text | |||
className="gradient-legend-text" | |||
key={idx} | |||
x={widthNoPadding * (idx / lastDomainIdx)} | |||
y={0} | |||
dy="-2px"> | |||
{d} | |||
</text> | |||
)} | |||
</g> | |||
{colorNA && | |||
<g transform={`translate(${widthNoPadding}, ${padding[0]})`}> | |||
<rect | |||
fill={colorNA} | |||
x={NA_SPACING} | |||
y={0} | |||
height={rectHeight} | |||
width={padding[1] - NA_SPACING} | |||
/> | |||
<text | |||
className="gradient-legend-na" | |||
x={NA_SPACING + (padding[1] - NA_SPACING) / 2} | |||
y={0} | |||
dy="-2px"> | |||
N/A | |||
</text> | |||
</g>} | |||
</svg> | |||
); | |||
} |
@@ -25,14 +25,14 @@ import { RATING_COLORS } from '../../helpers/constants'; | |||
export default function ColorRatingsLegend({ className }: { className?: string }) { | |||
return ( | |||
<div className={classNames('color-ratings-legend', className)}> | |||
<div className={classNames('color-box-legend', className)}> | |||
{[1, 2, 3, 4, 5].map(rating => | |||
<div key={rating}> | |||
<span | |||
className="color-ratings-legend-rect" | |||
className="color-box-legend-rect" | |||
style={{ borderColor: RATING_COLORS[rating - 1] }}> | |||
<span | |||
className="color-ratings-legend-rect-inner" | |||
className="color-box-legend-rect-inner" | |||
style={{ backgroundColor: RATING_COLORS[rating - 1] }} | |||
/> | |||
</span> |
@@ -263,7 +263,11 @@ | |||
text-anchor: end; | |||
} | |||
.color-ratings-legend { | |||
/* | |||
* Legends | |||
*/ | |||
.color-box-legend { | |||
display: flex; | |||
justify-content: center; | |||
@@ -271,7 +275,7 @@ | |||
margin-left: 24px; | |||
} | |||
.color-ratings-legend-rect { | |||
.color-box-legend-rect { | |||
display: inline-block; | |||
vertical-align: top; | |||
margin-top: 1px; | |||
@@ -279,12 +283,33 @@ | |||
border: 1px solid; | |||
} | |||
.color-ratings-legend-rect-inner { | |||
.color-box-legend-rect-inner { | |||
display: block; | |||
width: 8px; | |||
height: 8px; | |||
opacity: 0.2; | |||
} | |||
&.color-box-full .color-box-legend-rect-inner { | |||
opacity: 1; | |||
} | |||
} | |||
.gradient-legend-text, | |||
.gradient-legend-na { | |||
text-anchor: middle; | |||
fill: @secondFontColor; | |||
font-size: 10px; | |||
} | |||
.gradient-legend-text { | |||
&:first-of-type { | |||
text-anchor: start; | |||
} | |||
&:last-of-type { | |||
text-anchor: end; | |||
} | |||
} | |||
/* |