/* * SonarQube * Copyright (C) 2009-2023 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. */ /* eslint-disable @typescript-eslint/prefer-optional-chain */ import styled from '@emotion/styled'; import { max } from 'd3-array'; import { scaleBand, ScaleBand, scaleLinear, ScaleLinear } from 'd3-scale'; import React from 'react'; import tw from 'twin.macro'; import { themeColor, themeContrast } from '../helpers'; import Tooltip, { TooltipWrapper } from './Tooltip'; interface Props { bars: number[]; height: number; leftAlignTicks?: boolean; padding?: [number, number, number, number]; width: number; yTicks?: string[]; yTooltips?: string[]; yValues?: string[]; } const BAR_HEIGHT = 10; const DEFAULT_PADDING = [10, 10, 10, 10]; type XScale = ScaleLinear; type YScale = ScaleBand; export class Histogram extends React.PureComponent { renderBar(d: number, index: number, xScale: XScale, yScale: YScale) { const { leftAlignTicks, padding = DEFAULT_PADDING } = this.props; const width = Math.round(xScale(d)) + /* minimum bar width */ 1; const x = xScale.range()[0] + (leftAlignTicks ? padding[3] : 0); const y = Math.round((yScale(index) ?? 0) + yScale.bandwidth() / 2); return ; } renderValue(d: number, index: number, xScale: XScale, yScale: YScale) { const { leftAlignTicks, padding = DEFAULT_PADDING, yValues } = this.props; const value = yValues && yValues[index]; if (!value) { return null; } const x = xScale(d) + (leftAlignTicks ? padding[3] : 0); const y = Math.round((yScale(index) ?? 0) + yScale.bandwidth() / 2 + BAR_HEIGHT / 2); return ( {value} ); } renderTick(index: number, xScale: XScale, yScale: YScale) { const { leftAlignTicks, yTicks } = this.props; const tick = yTicks && yTicks[index]; if (!tick) { return null; } const x = xScale.range()[0]; const y = Math.round((yScale(index) ?? 0) + yScale.bandwidth() / 2 + BAR_HEIGHT / 2); return ( {tick} ); } renderBars(xScale: XScale, yScale: YScale) { return ( {this.props.bars.map((d, index) => ( {this.renderBar(d, index, xScale, yScale)} {this.renderValue(d, index, xScale, yScale)} {this.renderTick(index, xScale, yScale)} ))} ); } render() { const { bars, height, leftAlignTicks, padding = DEFAULT_PADDING, width } = this.props; const availableWidth = width - padding[1] - padding[3]; const xScale: XScale = scaleLinear() .domain([0, max(bars) ?? 0]) .range([0, availableWidth]); const availableHeight = height - padding[0] - padding[2]; const yScale: YScale = scaleBand() .domain(bars.map((_, index) => index)) .rangeRound([0, availableHeight]); return ( {this.renderBars(xScale, yScale)} ); } } const HistogramTick = styled.text` ${tw`sw-body-sm`} fill: ${themeColor('pageContentLight')}; ${TooltipWrapper} & { fill: ${themeContrast('primary')}; } `; const HistogramBar = styled.rect` fill: ${themeColor('primary')}; `;