components/Visualization/VerticalBarChart/index.js
import React, {
PureComponent,
Fragment,
} from 'react';
import { select } from 'd3-selection';
import { schemeSet3 } from 'd3-scale-chromatic';
import { max } from 'd3-array';
import SvgSaver from 'svgsaver';
import {
scaleOrdinal,
scaleBand,
scaleLinear,
} from 'd3-scale';
import {
axisLeft,
axisBottom,
} from 'd3-axis';
import { _cs } from '@togglecorp/fujs';
import PropTypes from 'prop-types';
import Responsive from '../../General/Responsive';
import Float from '../../View/Float';
import { getStandardFilename } from '../../../utils/common';
import styles from './styles.scss';
const propTypes = {
/**
* Size of the parent element/component (passed by the Responsive hoc)
*/
boundingClientRect: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number,
}).isRequired,
/**
* Array of data elements each having a label and value
*/
data: PropTypes.arrayOf(PropTypes.object).isRequired,
/**
* Handle chart saving functionality
*/
setSaveFunction: PropTypes.func,
/**
* Select the value of element
*/
valueSelector: PropTypes.func.isRequired,
/**
* Select the label of element
*/
labelSelector: PropTypes.func.isRequired,
/**
* Padding between two bars as proportion to bar width
*/
bandPadding: PropTypes.number,
/**
* Select a color for each bar
*/
colorSelector: PropTypes.func,
/**
* Handle mouseover over a bar
*/
onBarMouseOver: PropTypes.func,
/**
* if true, show axis
*/
showAxis: PropTypes.bool,
/**
* Array of colors as hex color codes
*/
colorScheme: PropTypes.arrayOf(PropTypes.string),
/**
* if ture, tooltip is visible
*/
showTooltip: PropTypes.bool,
/**
* Handle the contents of tooltip
*/
tooltipContent: PropTypes.func,
/**
* Margins for the chart
*/
margins: PropTypes.shape({
top: PropTypes.number,
right: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
}),
className: PropTypes.string,
};
const defaultProps = {
setSaveFunction: undefined,
onBarMouseOver: undefined,
colorSelector: undefined,
bandPadding: 0.2,
colorScheme: schemeSet3,
showAxis: true,
showTooltip: false,
tooltipContent: undefined,
margins: {
top: 24,
right: 24,
bottom: 24,
left: 72,
},
className: '',
};
/**
* VerticalBarChart represents categorical data with vertical bars. Height of each bar represent
* the value of data element.
*/
class VerticalBarChart extends PureComponent {
static propTypes = propTypes;
static defaultProps = defaultProps;
constructor(props) {
super(props);
if (props.setSaveFunction) {
props.setSaveFunction(this.save);
}
}
componentDidMount() {
this.drawChart();
}
componentDidUpdate() {
this.redrawChart();
}
getColor = (d, colors) => {
const {
labelSelector,
colorSelector,
} = this.props;
if (colorSelector) {
return colorSelector(d);
}
return colors(labelSelector(d));
}
save = () => {
const svgsaver = new SvgSaver();
const { current: svgElement } = this.svgRef;
const svg = select(svgElement);
svgsaver.asSvg(svg.node(), `${getStandardFilename('barchart', 'graph')}.svg`);
}
redrawChart = () => {
const svg = select(this.svgRef);
svg.selectAll('*').remove();
svg.selectAll('*').remove();
this.drawChart();
}
handleMouseOver = (d) => {
const {
valueSelector,
labelSelector,
showTooltip,
tooltipContent,
onBarMouseOver,
} = this.props;
if (onBarMouseOver) {
onBarMouseOver(d);
}
if (showTooltip) {
const value = valueSelector(d);
const label = labelSelector(d);
const defaultTooltip = `
<span class="${styles.label}">
${label || ''}
</span>
<span class="${styles.value}">
${value || ''}
</span>
`;
const content = tooltipContent ? tooltipContent(d) : defaultTooltip;
this.tooltip.innerHTML = content;
const { style } = this.tooltip;
style.display = 'block';
}
}
handleMouseMove = () => {
const { showTooltip } = this.props;
if (showTooltip) {
const { style } = this.tooltip;
const { width, height } = this.tooltip.getBoundingClientRect();
// eslint-disable-next-line no-restricted-globals
const x = event.pageX;
// eslint-disable-next-line no-restricted-globals
const y = event.pageY;
const posX = x - (width / 2);
const posY = y - (height + 10);
style.top = `${posY}px`;
style.left = `${posX}px`;
}
}
handleMouseOut = () => {
const {
showTooltip,
} = this.props;
if (showTooltip) {
const { style } = this.tooltip;
style.display = 'none';
}
}
drawChart = () => {
const {
data,
boundingClientRect,
valueSelector,
labelSelector,
bandPadding,
margins,
showAxis,
colorScheme,
} = this.props;
if (!boundingClientRect.width || !data || data.length === 0) {
return;
}
const { width: fullWidth, height: fullHeight } = boundingClientRect;
const {
left = 0,
top = 0,
right = 0,
bottom = 0,
} = margins;
const width = fullWidth - left - right;
const height = fullHeight - top - bottom;
const colors = scaleOrdinal().range(colorScheme);
const group = select(this.svgRef)
.append('g')
.attr('transform', `translate(${left}, ${top})`);
const x = scaleBand()
.domain(data.map(d => labelSelector(d)))
.rangeRound([0, width])
.padding(bandPadding);
const y = scaleLinear()
.domain([0, max(data, valueSelector)])
.range([height, 0])
.clamp(true);
group
.selectAll('.bar')
.data(data)
.enter()
.append('rect')
.attr('class', `bar ${styles.bar}`)
.attr('x', d => x(labelSelector(d)))
.attr('y', d => y(valueSelector(d)))
.attr('width', x.bandwidth())
.attr('height', d => height - y(valueSelector(d)))
.style('fill', d => this.getColor(d, colors))
.on('mouseover', d => this.handleMouseOver(d))
.on('mousemove', this.handleMouseMove)
.on('mouseout', this.handleMouseOut);
if (showAxis) {
const xAxis = axisBottom(x);
const yAxis = axisLeft(y);
group
.append('g')
.attr('class', `xaxis ${styles.xaxis}`)
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
group
.append('g')
.attr('class', `yaxis ${styles.yaxis}`)
.call(yAxis);
}
}
render() {
const { className: classNameFromProps } = this.props;
const className = _cs(
'vertical-bar-chart',
styles.verticalBarChart,
classNameFromProps,
);
const tooltipClassName = _cs(
'tooltip',
styles.tooltip,
);
return (
<Fragment>
<svg
className={className}
ref={(elem) => { this.svgRef = elem; }}
/>
<Float>
<div
className={tooltipClassName}
ref={(elem) => { this.tooltip = elem; }}
/>
</Float>
</Fragment>
);
}
}
export default Responsive(VerticalBarChart);