import { scaleLinear, scaleOrdinal } from 'd3-scale'; | import { scaleLinear, scaleOrdinal } from 'd3-scale'; | ||||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | import QualifierIcon from '../../../components/shared/QualifierIcon'; | ||||
import TreeMap from '../../../components/charts/TreeMap'; | 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 { translate, translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n'; | ||||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | ||||
import { getComponentUrl } from '../../../helpers/urls'; | import { getComponentUrl } from '../../../helpers/urls'; | ||||
}; | }; | ||||
const HEIGHT = 500; | const HEIGHT = 500; | ||||
const COLORS = ['#00aa00', '#b0d513', '#eabe06', '#ed7d20', '#d4333f']; | |||||
const LEVEL_COLORS = ['#d4333f', '#ed7d20', '#00aa00', '#b4b4b4']; | |||||
export default class TreeMapView extends React.PureComponent { | export default class TreeMapView extends React.PureComponent { | ||||
props: Props; | props: Props; | ||||
} | } | ||||
componentWillReceiveProps(nextProps: Props) { | 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) }); | this.setState({ treemapItems: this.getTreemapComponents(nextProps) }); | ||||
} | } | ||||
} | } | ||||
}; | }; | ||||
getLevelColorScale = () => | getLevelColorScale = () => | ||||
scaleOrdinal() | |||||
.domain(['ERROR', 'WARN', 'OK', 'NONE']) | |||||
.range(['#d4333f', '#ed7d20', '#00aa00', '#b4b4b4']); | |||||
scaleOrdinal().domain(['ERROR', 'WARN', 'OK', 'NONE']).range(LEVEL_COLORS); | |||||
getPercentColorScale = (metric: Metric) => { | getPercentColorScale = (metric: Metric) => { | ||||
const color = scaleLinear().domain([0, 25, 50, 75, 100]); | 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; | 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) => { | getColorScale = (metric: Metric) => { | ||||
if (metric.type === 'LEVEL') { | if (metric.type === 'LEVEL') { | ||||
); | ); | ||||
}; | }; | ||||
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() { | render() { | ||||
return ( | return ( | ||||
<div className="measure-details-treemap"> | <div className="measure-details-treemap"> | ||||
translate('metric.ncloc.name') | translate('metric.ncloc.name') | ||||
)} | )} | ||||
</li> | </li> | ||||
<li className="pull-right"> | |||||
{this.renderLegend()} | |||||
</li> | |||||
</ul> | </ul> | ||||
<AutoSizer> | <AutoSizer> | ||||
{({ width }) => | {({ width }) => |
font-size: 12px; | font-size: 12px; | ||||
} | } | ||||
.measure-details-treemap-legend { | |||||
margin-right: -4px; | |||||
} | |||||
.measure-details-treemap-legend.color-box-legend { | |||||
margin-right: 0; | |||||
} | |||||
.measure-view-select { | .measure-view-select { | ||||
width: 50px; | width: 50px; | ||||
} | } |
/* | |||||
* 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> | |||||
); | |||||
} |
/* | |||||
* 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> | |||||
); | |||||
} |
export default function ColorRatingsLegend({ className }: { className?: string }) { | export default function ColorRatingsLegend({ className }: { className?: string }) { | ||||
return ( | return ( | ||||
<div className={classNames('color-ratings-legend', className)}> | |||||
<div className={classNames('color-box-legend', className)}> | |||||
{[1, 2, 3, 4, 5].map(rating => | {[1, 2, 3, 4, 5].map(rating => | ||||
<div key={rating}> | <div key={rating}> | ||||
<span | <span | ||||
className="color-ratings-legend-rect" | |||||
className="color-box-legend-rect" | |||||
style={{ borderColor: RATING_COLORS[rating - 1] }}> | style={{ borderColor: RATING_COLORS[rating - 1] }}> | ||||
<span | <span | ||||
className="color-ratings-legend-rect-inner" | |||||
className="color-box-legend-rect-inner" | |||||
style={{ backgroundColor: RATING_COLORS[rating - 1] }} | style={{ backgroundColor: RATING_COLORS[rating - 1] }} | ||||
/> | /> | ||||
</span> | </span> |
text-anchor: end; | text-anchor: end; | ||||
} | } | ||||
.color-ratings-legend { | |||||
/* | |||||
* Legends | |||||
*/ | |||||
.color-box-legend { | |||||
display: flex; | display: flex; | ||||
justify-content: center; | justify-content: center; | ||||
margin-left: 24px; | margin-left: 24px; | ||||
} | } | ||||
.color-ratings-legend-rect { | |||||
.color-box-legend-rect { | |||||
display: inline-block; | display: inline-block; | ||||
vertical-align: top; | vertical-align: top; | ||||
margin-top: 1px; | margin-top: 1px; | ||||
border: 1px solid; | border: 1px solid; | ||||
} | } | ||||
.color-ratings-legend-rect-inner { | |||||
.color-box-legend-rect-inner { | |||||
display: block; | display: block; | ||||
width: 8px; | width: 8px; | ||||
height: 8px; | height: 8px; | ||||
opacity: 0.2; | 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; | |||||
} | |||||
} | } | ||||
/* | /* |