DashboardBarChart.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import * as React from 'react'
  15. import { observer } from 'mobx-react'
  16. import styled from 'styled-components'
  17. import StyleProps from '../../../styleUtils/StyleProps'
  18. import BarChartNiceScale from './BarChartNiceScale'
  19. const Wrapper = styled.div<any>`
  20. position: relative;
  21. width: 100%;
  22. `
  23. const YAxis = styled.div<any>`
  24. height: calc(100% - 24px);
  25. position: absolute;
  26. bottom: 24px;
  27. left: 16px;
  28. `
  29. const YTick = styled.div<any>`
  30. position: absolute;
  31. top: ${props => 100 - props.bottom}%;
  32. font-size: 9px;
  33. font-weight: ${StyleProps.fontWeights.medium};
  34. width: 24px;
  35. overflow: hidden;
  36. text-overflow: ellipsis;
  37. text-align: right;
  38. `
  39. const GridLines = styled.div<any>`
  40. width: calc(100% - 64px);
  41. height: calc(100% - 24px);
  42. position: absolute;
  43. bottom: 19px;
  44. left: 48px;
  45. `
  46. const GridLine = styled.div<any>`
  47. position: absolute;
  48. bottom: ${props => props.bottom}%;
  49. height: 1px;
  50. width: 100%;
  51. background: white;
  52. `
  53. const Bars = styled.div<any>`
  54. position: absolute;
  55. display: flex;
  56. height: calc(100% - 6px);
  57. width: calc(100% - 64px);
  58. justify-content: space-around;
  59. left: 48px;
  60. bottom: 2px;
  61. `
  62. const Bar = styled.div<any>`
  63. display: flex;
  64. flex-direction: column;
  65. align-items: center;
  66. `
  67. const StackedBars = styled.div<any>`
  68. display: flex;
  69. flex-direction: column;
  70. height: 100%;
  71. justify-content: flex-end;
  72. `
  73. const StackedBar = styled.div<any>`
  74. width: 16px;
  75. height: ${props => props.height}%;
  76. background: ${props => props.background};
  77. &:first-child {
  78. border-top-left-radius: 3px;
  79. border-top-right-radius: 3px;
  80. }
  81. `
  82. const BarLabel = styled.div<any>`
  83. font-size: 9px;
  84. font-weight: ${StyleProps.fontWeights.medium};
  85. margin-top: 8px;
  86. `
  87. type DataItem = {
  88. label: string,
  89. values: number[],
  90. data?: any,
  91. }
  92. type Props = {
  93. style?: any,
  94. // eslint-disable-next-line react/no-unused-prop-types
  95. data: DataItem[],
  96. // eslint-disable-next-line react/no-unused-prop-types
  97. yNumTicks: number,
  98. colors?: string[],
  99. onBarMouseEnter?: (position: { x: number, y: number }, item: DataItem) => void,
  100. onBarMouseLeave?: () => void,
  101. }
  102. @observer
  103. class DashboardBarChart extends React.Component<Props> {
  104. barsRef: HTMLElement | null | undefined
  105. ticks: { value: number }[] = []
  106. range: number = 1
  107. UNSAFE_componentWillMount() {
  108. this.calculateYTicks(this.props)
  109. }
  110. UNSAFE_componentWillReceiveProps(props: Props) {
  111. this.calculateYTicks(props)
  112. }
  113. calculateYTicks(props: Props) {
  114. this.range = props.data.reduce((max, item) => Math.max(
  115. max, item.values.reduce((sum, value) => sum + value, 0),
  116. ), 1)
  117. const niceScale = new BarChartNiceScale(0, this.range, props.yNumTicks)
  118. this.ticks = []
  119. const numTicks = Math.floor(this.range / niceScale.tickSpacing) + 1
  120. for (let i = 0; i < numTicks; i += 1) {
  121. this.ticks.push({ value: i * niceScale.tickSpacing })
  122. }
  123. }
  124. calculatePosition(evt: MouseEvent): { x: number, y: number } {
  125. const targetMouse: any = evt.currentTarget
  126. const target: HTMLElement = targetMouse.parentElement
  127. let height = 0
  128. target.childNodes.forEach(node => {
  129. const element: any = node
  130. height += element.offsetHeight
  131. })
  132. return {
  133. x: target.offsetLeft + 48,
  134. y: height + 65,
  135. }
  136. }
  137. renderYAxis() {
  138. return (
  139. <YAxis>
  140. {this.ticks.map(tick => (
  141. <YTick key={tick.value} bottom={(tick.value / this.range) * 100}>{tick.value}</YTick>
  142. ))}
  143. </YAxis>
  144. )
  145. }
  146. renderGridLines() {
  147. const gridLines: { value: number }[] = []
  148. this.ticks.forEach((tick, i) => {
  149. gridLines.push({ value: tick.value })
  150. if (i === this.ticks.length - 1) {
  151. return
  152. }
  153. gridLines.push({ value: (this.ticks[i + 1].value + tick.value) / 2 })
  154. })
  155. return (
  156. <GridLines>
  157. {gridLines.map(gridline => (
  158. <GridLine key={gridline.value} bottom={(gridline.value / this.range) * 100} />
  159. ))}
  160. </GridLines>
  161. )
  162. }
  163. renderBars() {
  164. let availableWidth = window.innerWidth
  165. if (this.barsRef) {
  166. availableWidth = this.barsRef.offsetWidth
  167. }
  168. let items = this.props.data
  169. if ((30 * items.length) > availableWidth) {
  170. items = items.filter((_, i) => i % 2)
  171. }
  172. return (
  173. <Bars ref={(ref: HTMLElement | null | undefined) => { this.barsRef = ref }}>
  174. {items.map(item => (
  175. <Bar key={item.label}>
  176. <StackedBars>
  177. {[...item.values].reverse().map((value, i) => {
  178. const height = (value / this.range) * 100
  179. return height > 0 ? (
  180. <StackedBar
  181. // eslint-disable-next-line react/no-array-index-key
  182. key={`${item.label}-${i}`}
  183. background={this.props.colors ? this.props.colors[i % this.props.colors.length] : '#0044CA'}
  184. height={height}
  185. onMouseEnter={(evt: MouseEvent) => {
  186. const onMouseEnter = this.props.onBarMouseEnter
  187. if (!onMouseEnter) {
  188. return
  189. }
  190. onMouseEnter(this.calculatePosition(evt), item)
  191. }}
  192. onMouseLeave={() => {
  193. if (this.props.onBarMouseLeave) this.props.onBarMouseLeave()
  194. }}
  195. />
  196. ) : null
  197. })}
  198. </StackedBars>
  199. <BarLabel>{item.label}</BarLabel>
  200. </Bar>
  201. ))}
  202. </Bars>
  203. )
  204. }
  205. render() {
  206. return (
  207. <Wrapper style={this.props.style}>
  208. {this.renderYAxis()}
  209. {this.renderGridLines()}
  210. {this.renderBars()}
  211. </Wrapper>
  212. )
  213. }
  214. }
  215. export default DashboardBarChart