| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- /*
- Copyright (C) 2017 Cloudbase Solutions SRL
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero 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 Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- import * as React from 'react'
- import { observer } from 'mobx-react'
- import styled from 'styled-components'
- import StyleProps from '../../../styleUtils/StyleProps'
- import BarChartNiceScale from './BarChartNiceScale'
- const Wrapper = styled.div<any>`
- position: relative;
- width: 100%;
- `
- const YAxis = styled.div<any>`
- height: calc(100% - 24px);
- position: absolute;
- bottom: 24px;
- left: 16px;
- `
- const YTick = styled.div<any>`
- position: absolute;
- top: ${props => 100 - props.bottom}%;
- font-size: 9px;
- font-weight: ${StyleProps.fontWeights.medium};
- width: 24px;
- overflow: hidden;
- text-overflow: ellipsis;
- text-align: right;
- `
- const GridLines = styled.div<any>`
- width: calc(100% - 64px);
- height: calc(100% - 24px);
- position: absolute;
- bottom: 19px;
- left: 48px;
- `
- const GridLine = styled.div<any>`
- position: absolute;
- bottom: ${props => props.bottom}%;
- height: 1px;
- width: 100%;
- background: white;
- `
- const Bars = styled.div<any>`
- position: absolute;
- display: flex;
- height: calc(100% - 6px);
- width: calc(100% - 64px);
- justify-content: space-around;
- left: 48px;
- bottom: 2px;
- `
- const Bar = styled.div<any>`
- display: flex;
- flex-direction: column;
- align-items: center;
- `
- const StackedBars = styled.div<any>`
- display: flex;
- flex-direction: column;
- height: 100%;
- justify-content: flex-end;
- `
- const StackedBar = styled.div<any>`
- width: 16px;
- height: ${props => props.height}%;
- background: ${props => props.background};
- &:first-child {
- border-top-left-radius: 3px;
- border-top-right-radius: 3px;
- }
- `
- const BarLabel = styled.div<any>`
- font-size: 9px;
- font-weight: ${StyleProps.fontWeights.medium};
- margin-top: 8px;
- `
- type DataItem = {
- label: string,
- values: number[],
- data?: any,
- }
- type Props = {
- style?: any,
- // eslint-disable-next-line react/no-unused-prop-types
- data: DataItem[],
- // eslint-disable-next-line react/no-unused-prop-types
- yNumTicks: number,
- colors?: string[],
- onBarMouseEnter?: (position: { x: number, y: number }, item: DataItem) => void,
- onBarMouseLeave?: () => void,
- }
- @observer
- class DashboardBarChart extends React.Component<Props> {
- barsRef: HTMLElement | null | undefined
- ticks: { value: number }[] = []
- range: number = 1
- UNSAFE_componentWillMount() {
- this.calculateYTicks(this.props)
- }
- UNSAFE_componentWillReceiveProps(props: Props) {
- this.calculateYTicks(props)
- }
- calculateYTicks(props: Props) {
- this.range = props.data.reduce((max, item) => Math.max(
- max, item.values.reduce((sum, value) => sum + value, 0),
- ), 1)
- const niceScale = new BarChartNiceScale(0, this.range, props.yNumTicks)
- this.ticks = []
- const numTicks = Math.floor(this.range / niceScale.tickSpacing) + 1
- for (let i = 0; i < numTicks; i += 1) {
- this.ticks.push({ value: i * niceScale.tickSpacing })
- }
- }
- calculatePosition(evt: MouseEvent): { x: number, y: number } {
- const targetMouse: any = evt.currentTarget
- const target: HTMLElement = targetMouse.parentElement
- let height = 0
- target.childNodes.forEach(node => {
- const element: any = node
- height += element.offsetHeight
- })
- return {
- x: target.offsetLeft + 48,
- y: height + 65,
- }
- }
- renderYAxis() {
- return (
- <YAxis>
- {this.ticks.map(tick => (
- <YTick key={tick.value} bottom={(tick.value / this.range) * 100}>{tick.value}</YTick>
- ))}
- </YAxis>
- )
- }
- renderGridLines() {
- const gridLines: { value: number }[] = []
- this.ticks.forEach((tick, i) => {
- gridLines.push({ value: tick.value })
- if (i === this.ticks.length - 1) {
- return
- }
- gridLines.push({ value: (this.ticks[i + 1].value + tick.value) / 2 })
- })
- return (
- <GridLines>
- {gridLines.map(gridline => (
- <GridLine key={gridline.value} bottom={(gridline.value / this.range) * 100} />
- ))}
- </GridLines>
- )
- }
- renderBars() {
- let availableWidth = window.innerWidth
- if (this.barsRef) {
- availableWidth = this.barsRef.offsetWidth
- }
- let items = this.props.data
- if ((30 * items.length) > availableWidth) {
- items = items.filter((_, i) => i % 2)
- }
- return (
- <Bars ref={(ref: HTMLElement | null | undefined) => { this.barsRef = ref }}>
- {items.map(item => (
- <Bar key={item.label}>
- <StackedBars>
- {[...item.values].reverse().map((value, i) => {
- const height = (value / this.range) * 100
- return height > 0 ? (
- <StackedBar
- // eslint-disable-next-line react/no-array-index-key
- key={`${item.label}-${i}`}
- background={this.props.colors ? this.props.colors[i % this.props.colors.length] : '#0044CA'}
- height={height}
- onMouseEnter={(evt: MouseEvent) => {
- const onMouseEnter = this.props.onBarMouseEnter
- if (!onMouseEnter) {
- return
- }
- onMouseEnter(this.calculatePosition(evt), item)
- }}
- onMouseLeave={() => {
- if (this.props.onBarMouseLeave) this.props.onBarMouseLeave()
- }}
- />
- ) : null
- })}
- </StackedBars>
- <BarLabel>{item.label}</BarLabel>
- </Bar>
- ))}
- </Bars>
- )
- }
- render() {
- return (
- <Wrapper style={this.props.style}>
- {this.renderYAxis()}
- {this.renderGridLines()}
- {this.renderBars()}
- </Wrapper>
- )
- }
- }
- export default DashboardBarChart
|