You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TreeMapView.tsx 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { scaleLinear, scaleOrdinal } from 'd3-scale';
  21. import * as React from 'react';
  22. import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
  23. import { colors } from '../../../app/theme';
  24. import ColorBoxLegend from '../../../components/charts/ColorBoxLegend';
  25. import ColorGradientLegend from '../../../components/charts/ColorGradientLegend';
  26. import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap';
  27. import QualifierIcon from '../../../components/icons/QualifierIcon';
  28. import { getComponentMeasureUniqueKey } from '../../../helpers/component';
  29. import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
  30. import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
  31. import { isDefined } from '../../../helpers/types';
  32. import { BranchLike } from '../../../types/branch-like';
  33. import { MetricKey } from '../../../types/metrics';
  34. import EmptyResult from './EmptyResult';
  35. interface Props {
  36. branchLike?: BranchLike;
  37. components: T.ComponentMeasureEnhanced[];
  38. handleSelect: (component: T.ComponentMeasureIntern) => void;
  39. metric: T.Metric;
  40. }
  41. interface State {
  42. treemapItems: TreeMapItem[];
  43. }
  44. const HEIGHT = 500;
  45. const COLORS = [colors.green, colors.lightGreen, colors.yellow, colors.orange, colors.red];
  46. const LEVEL_COLORS = [colors.red, colors.orange, colors.green, colors.gray71];
  47. const NA_GRADIENT = `linear-gradient(-45deg, ${colors.gray71} 25%, ${colors.gray60} 25%, ${colors.gray60} 50%, ${colors.gray71} 50%, ${colors.gray71} 75%, ${colors.gray60} 75%, ${colors.gray60} 100%)`;
  48. export default class TreeMapView extends React.PureComponent<Props, State> {
  49. state: State;
  50. constructor(props: Props) {
  51. super(props);
  52. this.state = { treemapItems: this.getTreemapComponents(props) };
  53. }
  54. componentDidUpdate(prevProps: Props) {
  55. if (prevProps.components !== this.props.components || prevProps.metric !== this.props.metric) {
  56. this.setState({ treemapItems: this.getTreemapComponents(this.props) });
  57. }
  58. }
  59. getTreemapComponents = ({ components, metric }: Props) => {
  60. const colorScale = this.getColorScale(metric);
  61. return components
  62. .map(component => {
  63. const colorMeasure = component.measures.find(measure => measure.metric.key === metric.key);
  64. const sizeMeasure = component.measures.find(measure => measure.metric.key !== metric.key);
  65. if (!sizeMeasure) {
  66. return undefined;
  67. }
  68. const colorValue =
  69. colorMeasure && (isDiffMetric(metric.key) ? colorMeasure.leak : colorMeasure.value);
  70. const rawSizeValue = isDiffMetric(sizeMeasure.metric.key)
  71. ? sizeMeasure.leak
  72. : sizeMeasure.value;
  73. if (rawSizeValue === undefined) {
  74. return undefined;
  75. }
  76. const sizeValue = Number(rawSizeValue);
  77. if (sizeValue < 1) {
  78. return undefined;
  79. }
  80. return {
  81. color: colorValue ? (colorScale as Function)(colorValue) : undefined,
  82. gradient: !colorValue ? NA_GRADIENT : undefined,
  83. icon: <QualifierIcon fill={colors.baseFontColor} qualifier={component.qualifier} />,
  84. key: getComponentMeasureUniqueKey(component) ?? '',
  85. label: [component.name, component.branch].filter(s => !!s).join(' / '),
  86. size: sizeValue,
  87. measureValue: colorValue,
  88. metric,
  89. tooltip: this.getTooltip({
  90. colorMetric: metric,
  91. colorValue,
  92. component,
  93. sizeMetric: sizeMeasure.metric,
  94. sizeValue
  95. }),
  96. component
  97. };
  98. })
  99. .filter(isDefined);
  100. };
  101. getLevelColorScale = () =>
  102. scaleOrdinal<string, string>()
  103. .domain(['ERROR', 'WARN', 'OK', 'NONE'])
  104. .range(LEVEL_COLORS);
  105. getPercentColorScale = (metric: T.Metric) => {
  106. const color = scaleLinear<string, string>().domain([0, 25, 50, 75, 100]);
  107. color.range(metric.higherValuesAreBetter ? [...COLORS].reverse() : COLORS);
  108. return color;
  109. };
  110. getRatingColorScale = () =>
  111. scaleLinear<string, string>()
  112. .domain([1, 2, 3, 4, 5])
  113. .range(COLORS);
  114. getColorScale = (metric: T.Metric) => {
  115. if (metric.type === 'LEVEL') {
  116. return this.getLevelColorScale();
  117. }
  118. if (metric.type === 'RATING') {
  119. return this.getRatingColorScale();
  120. }
  121. return this.getPercentColorScale(metric);
  122. };
  123. getTooltip = ({
  124. colorMetric,
  125. colorValue,
  126. component,
  127. sizeMetric,
  128. sizeValue
  129. }: {
  130. colorMetric: T.Metric;
  131. colorValue?: string;
  132. component: T.ComponentMeasureEnhanced;
  133. sizeMetric: T.Metric;
  134. sizeValue: number;
  135. }) => {
  136. const formatted =
  137. colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—';
  138. return (
  139. <div className="text-left">
  140. {[component.name, component.branch].filter(s => !!s).join(' / ')}
  141. <br />
  142. {`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`}
  143. <br />
  144. {`${getLocalizedMetricName(colorMetric)}: ${formatted}`}
  145. </div>
  146. );
  147. };
  148. renderLegend() {
  149. const { metric } = this.props;
  150. const colorScale = this.getColorScale(metric);
  151. if (['LEVEL', 'RATING'].includes(metric.type)) {
  152. return (
  153. <ColorBoxLegend
  154. className="measure-details-treemap-legend color-box-full"
  155. colorScale={colorScale}
  156. metricType={metric.type}
  157. />
  158. );
  159. }
  160. return (
  161. <ColorGradientLegend
  162. className="measure-details-treemap-legend"
  163. showColorNA={true}
  164. colorScale={colorScale}
  165. height={30}
  166. width={200}
  167. />
  168. );
  169. }
  170. render() {
  171. const { treemapItems } = this.state;
  172. if (treemapItems.length <= 0) {
  173. return <EmptyResult />;
  174. }
  175. const { components, metric } = this.props;
  176. const sizeMeasure =
  177. components.length > 0
  178. ? components[0].measures.find(measure => measure.metric.key !== metric.key)
  179. : null;
  180. return (
  181. <div className="measure-details-treemap">
  182. <div className="display-flex-start note spacer-bottom">
  183. <span>
  184. {translateWithParameters(
  185. 'component_measures.legend.color_x',
  186. getLocalizedMetricName(metric)
  187. )}
  188. </span>
  189. <span className="spacer-left flex-1">
  190. {translateWithParameters(
  191. 'component_measures.legend.size_x',
  192. translate(
  193. 'metric',
  194. sizeMeasure && sizeMeasure.metric ? sizeMeasure.metric.key : MetricKey.ncloc,
  195. 'name'
  196. )
  197. )}
  198. </span>
  199. <span>{this.renderLegend()}</span>
  200. </div>
  201. <AutoSizer disableHeight={true}>
  202. {({ width }) => (
  203. <TreeMap
  204. height={HEIGHT}
  205. items={treemapItems}
  206. onRectangleClick={this.props.handleSelect}
  207. width={width}
  208. />
  209. )}
  210. </AutoSizer>
  211. </div>
  212. );
  213. }
  214. }