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.

BarChart.tsx 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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 { max } from 'd3-array';
  21. import { scaleBand, ScaleBand, scaleLinear, ScaleLinear } from 'd3-scale';
  22. import * as React from 'react';
  23. import Tooltip from '../controls/Tooltip';
  24. import './BarChart.css';
  25. interface DataPoint {
  26. tooltip?: React.ReactNode;
  27. x: number;
  28. y: number;
  29. }
  30. interface Props<T> {
  31. barsWidth: number;
  32. data: Array<DataPoint & T>;
  33. height: number;
  34. onBarClick?: (point: DataPoint & T) => void;
  35. padding?: [number, number, number, number];
  36. width: number;
  37. xTicks?: string[];
  38. xValues?: string[];
  39. }
  40. export default class BarChart<T> extends React.PureComponent<Props<T>> {
  41. handleClick = (point: DataPoint & T) => {
  42. if (this.props.onBarClick) {
  43. this.props.onBarClick(point);
  44. }
  45. };
  46. renderXTicks = (xScale: ScaleBand<number>, yScale: ScaleLinear<number, number>) => {
  47. const { data, xTicks = [] } = this.props;
  48. if (!xTicks.length) {
  49. return null;
  50. }
  51. const ticks = xTicks.map((tick, index) => {
  52. const point = data[index];
  53. const x = Math.round((xScale(point.x) as number) + xScale.bandwidth() / 2);
  54. const y = yScale.range()[0];
  55. const d = data[index];
  56. const text = (
  57. <text
  58. className="bar-chart-tick"
  59. dy="1.5em"
  60. key={index}
  61. onClick={() => this.handleClick(point)}
  62. style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
  63. x={x}
  64. y={y}>
  65. {tick}
  66. </text>
  67. );
  68. return (
  69. <Tooltip key={index} overlay={d.tooltip || undefined}>
  70. {text}
  71. </Tooltip>
  72. );
  73. });
  74. return <g>{ticks}</g>;
  75. };
  76. renderXValues = (xScale: ScaleBand<number>, yScale: ScaleLinear<number, number>) => {
  77. const { data, xValues = [] } = this.props;
  78. if (!xValues.length) {
  79. return null;
  80. }
  81. const ticks = xValues.map((value, index) => {
  82. const point = data[index];
  83. const x = Math.round((xScale(point.x) as number) + xScale.bandwidth() / 2);
  84. const y = yScale(point.y);
  85. const text = (
  86. <text
  87. className="bar-chart-tick"
  88. dy="-1em"
  89. key={index}
  90. onClick={() => this.handleClick(point)}
  91. style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
  92. x={x}
  93. y={y}>
  94. {value}
  95. </text>
  96. );
  97. return (
  98. <Tooltip key={index} overlay={point.tooltip || undefined}>
  99. {text}
  100. </Tooltip>
  101. );
  102. });
  103. return <g>{ticks}</g>;
  104. };
  105. renderBars = (xScale: ScaleBand<number>, yScale: ScaleLinear<number, number>) => {
  106. const bars = this.props.data.map((point, index) => {
  107. const x = Math.round(xScale(point.x) as number);
  108. const maxY = yScale.range()[0];
  109. const y = Math.round(yScale(point.y)) - /* minimum bar height */ 1;
  110. const height = maxY - y;
  111. const rect = (
  112. <rect
  113. className="bar-chart-bar"
  114. height={height}
  115. key={index}
  116. onClick={() => this.handleClick(point)}
  117. style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
  118. width={this.props.barsWidth}
  119. x={x}
  120. y={y}
  121. />
  122. );
  123. return (
  124. <Tooltip key={index} overlay={point.tooltip || undefined}>
  125. {rect}
  126. </Tooltip>
  127. );
  128. });
  129. return <g>{bars}</g>;
  130. };
  131. render() {
  132. const { barsWidth, data, width, height, padding = [10, 10, 10, 10] } = this.props;
  133. const availableWidth = width - padding[1] - padding[3];
  134. const availableHeight = height - padding[0] - padding[2];
  135. const innerPadding = (availableWidth - barsWidth * data.length) / (data.length - 1);
  136. const relativeInnerPadding = innerPadding / (innerPadding + barsWidth);
  137. const maxY = max(data, (d) => d.y) as number;
  138. const xScale = scaleBand<number>()
  139. .domain(data.map((d) => d.x))
  140. .range([0, availableWidth])
  141. .paddingInner(relativeInnerPadding);
  142. const yScale = scaleLinear().domain([0, maxY]).range([availableHeight, 0]);
  143. return (
  144. <svg className="bar-chart" height={height} width={width}>
  145. <g transform={`translate(${padding[3]}, ${padding[0]})`}>
  146. {this.renderXTicks(xScale, yScale)}
  147. {this.renderXValues(xScale, yScale)}
  148. {this.renderBars(xScale, yScale)}
  149. </g>
  150. </svg>
  151. );
  152. }
  153. }