From 01570b82ed0d332f930a94953d23579882bd522a Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Fri, 26 May 2023 08:37:44 +0200 Subject: [PATCH] SONAR-19387 Add new Histogram component --- .../src/components/Histogram.tsx | 151 +++++++ .../components/__tests__/Histogram-test.tsx | 53 +++ .../__snapshots__/Histogram-test.tsx.snap | 375 ++++++++++++++++++ .../design-system/src/components/index.ts | 1 + 4 files changed, 580 insertions(+) create mode 100644 server/sonar-web/design-system/src/components/Histogram.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/Histogram-test.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap diff --git a/server/sonar-web/design-system/src/components/Histogram.tsx b/server/sonar-web/design-system/src/components/Histogram.tsx new file mode 100644 index 00000000000..7298c690c87 --- /dev/null +++ b/server/sonar-web/design-system/src/components/Histogram.tsx @@ -0,0 +1,151 @@ +/* + * 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')}; +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/Histogram-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Histogram-test.tsx new file mode 100644 index 00000000000..71fdab44724 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/Histogram-test.tsx @@ -0,0 +1,53 @@ +/* + * 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. + */ + +import { render } from '../../helpers/testUtils'; +import { Histogram } from '../Histogram'; + +it('renders correctly', () => { + const { container } = renderHistogram(); + expect(container).toMatchSnapshot(); +}); + +it('renders correctly with yValues', () => { + const { container } = renderHistogram({ yValues: ['100.0', '75.0', '150.0'] }); + expect(container).toMatchSnapshot(); +}); + +it('renders correctly with yValues and yTicks', () => { + const { container } = renderHistogram({ + yValues: ['100.0', '75.0', '150.0'], + yTicks: ['a', 'b', 'c'], + }); + expect(container).toMatchSnapshot(); +}); + +it('renders correctly with yValues, yTicks, and yTooltips', () => { + const { container } = renderHistogram({ + yValues: ['100.0', '75.0', '150.0'], + yTicks: ['a', 'b', 'c'], + yTooltips: ['a - 100', 'b - 75', 'c - 150'], + }); + expect(container).toMatchSnapshot(); +}); + +function renderHistogram(props: Partial = {}) { + return render(); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap new file mode 100644 index 00000000000..166209b1591 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap @@ -0,0 +1,375 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +.emotion-0 { + fill: rgb(93,108,208); +} + +
+ + + + + + + + + + + + + + + +
+`; + +exports[`renders correctly with yValues 1`] = ` +.emotion-0 { + fill: rgb(93,108,208); +} + +.emotion-2 { + font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; + fill: rgb(106,117,144); +} + +.e1vbniy52 .emotion-2 { + fill: rgb(255,255,255); +} + +
+ + + + + + + 100.0 + + + + + + 75.0 + + + + + + 150.0 + + + + + +
+`; + +exports[`renders correctly with yValues and yTicks 1`] = ` +.emotion-0 { + fill: rgb(93,108,208); +} + +.emotion-2 { + font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; + fill: rgb(106,117,144); +} + +.e1vbniy52 .emotion-2 { + fill: rgb(255,255,255); +} + +
+ + + + + + + 100.0 + + + a + + + + + + 75.0 + + + b + + + + + + 150.0 + + + c + + + + + +
+`; + +exports[`renders correctly with yValues, yTicks, and yTooltips 1`] = ` +.emotion-0 { + fill: rgb(93,108,208); +} + +.emotion-2 { + font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; + fill: rgb(106,117,144); +} + +.e1vbniy52 .emotion-2 { + fill: rgb(255,255,255); +} + +
+ + + + + + + 100.0 + + + a + + + + + + 75.0 + + + b + + + + + + 150.0 + + + c + + + + + +
+`; diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index bf3c48ca002..16fe359ece8 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -44,6 +44,7 @@ export * from './FlowStep'; export * from './FormField'; export * from './GenericAvatar'; export * from './HighlightedSection'; +export { Histogram } from './Histogram'; export { HotspotRating } from './HotspotRating'; export * from './HtmlFormatter'; export * from './InputField'; -- 2.39.5