From 6c3d97c42970c63b308ecac90f12b1bb01e8b429 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Tue, 8 Aug 2017 10:44:41 +0200 Subject: [PATCH] SONAR-9608 SONAR-9636 Add treemap legend on project measures page --- .../drilldown/TreeMapView.js | 48 +++++++--- .../main/js/apps/component-measures/style.css | 8 ++ .../js/components/charts/ColorBoxLegend.js | 57 +++++++++++ .../components/charts/ColorGradientLegend.js | 96 +++++++++++++++++++ .../components/charts/ColorRatingsLegend.js | 6 +- .../src/main/less/components/graphics.less | 31 +++++- 6 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/charts/ColorBoxLegend.js create mode 100644 server/sonar-web/src/main/js/components/charts/ColorGradientLegend.js diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js index 5555bb37041..3986f7c5520 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js @@ -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 ( + + ); + } + return ( + + ); + } + render() { return (
@@ -160,6 +179,9 @@ export default class TreeMapView extends React.PureComponent { translate('metric.ncloc.name') )} +
  • + {this.renderLegend()} +
  • {({ width }) => diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css index 9739bc6ef15..b9b172b9b77 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/style.css +++ b/server/sonar-web/src/main/js/apps/component-measures/style.css @@ -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; } diff --git a/server/sonar-web/src/main/js/components/charts/ColorBoxLegend.js b/server/sonar-web/src/main/js/components/charts/ColorBoxLegend.js new file mode 100644 index 00000000000..390b3ccc838 --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/ColorBoxLegend.js @@ -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 ( +
    + {colorDomain.map((value, idx) => +
    + + + + {formatMeasure(value, metricType)} +
    + )} + {colorNA && +
    + + + + N/A +
    } +
    + ); +} diff --git a/server/sonar-web/src/main/js/components/charts/ColorGradientLegend.js b/server/sonar-web/src/main/js/components/charts/ColorGradientLegend.js new file mode 100644 index 00000000000..18dec37a7d7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/ColorGradientLegend.js @@ -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, + 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 ( + + + + {colorRange.map((color, idx) => + + )} + + + + + {colorDomain.map((d, idx) => + + {d} + + )} + + {colorNA && + + + + N/A + + } + + ); +} diff --git a/server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js b/server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js index 9e17ff01add..a5caf0e534a 100644 --- a/server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js +++ b/server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js @@ -25,14 +25,14 @@ import { RATING_COLORS } from '../../helpers/constants'; export default function ColorRatingsLegend({ className }: { className?: string }) { return ( -
    +
    {[1, 2, 3, 4, 5].map(rating =>
    diff --git a/server/sonar-web/src/main/less/components/graphics.less b/server/sonar-web/src/main/less/components/graphics.less index 8df1fcb6b70..619e7a4ebdc 100644 --- a/server/sonar-web/src/main/less/components/graphics.less +++ b/server/sonar-web/src/main/less/components/graphics.less @@ -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; + } } /* -- 2.39.5