wix-style-react
Version:
285 lines (247 loc) • 7.61 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import Tooltip from '../Tooltip';
import { TooltipCommonProps } from '../common/PropTypes/TooltipCommon';
import { st, classes } from './StackedBarChart.st.css';
import { stVars as colors } from '../Foundation/stylable/colors.st.css';
import { scaleLinear, scaleBand } from 'd3-scale';
import { select } from 'd3-selection';
import { axisRight } from 'd3-axis';
import { format } from 'd3-format';
import { dataHooks } from './constants';
import Text from '../Text';
const defaultYAxisTickFormat = format(',');
const CONSTANTS = {
top: 30,
right: 30,
bottom: 30,
left: 100,
gap: 18,
barWidth: 48,
};
/** StackedBarChart */
class StackedBarChart extends React.PureComponent {
constructor(props) {
super(props);
this.chart = React.createRef();
window.chart = this;
this.data = [];
this.colors = [colors.A1, colors.A6];
this.margin = Object.assign(CONSTANTS, props.margin);
this.height = props.height;
this.chartHeight = this.height - this.margin.top - this.margin.bottom;
this.state = {
data: [],
x: scaleBand().range([0, props.weight]),
y: scaleLinear().range([this.chartHeight, 0]),
width: props.weight,
};
}
_getAmountOfBarsCanRender = data => {
const { width } = this.props;
const result = [];
const availableWidthForBars = width - this.margin.left;
const barWidth = CONSTANTS.barWidth + CONSTANTS.gap;
const availableBarsForRender = Math.floor(availableWidthForBars / barWidth);
for (let i = 0; i < availableBarsForRender; i++) {
if (!!data[i]) {
result.push(data[i]);
}
}
return result;
};
_update = () => {
const { data, yAxisTickFormat, width } = this.props;
const { svg, x, y } = this.state;
// Data
const _data = this._getAmountOfBarsCanRender(data).map(d => ({
...d,
sum: d.values.reduce((a, b) => a + b, 0),
}));
const innerPadding = _data.length > 4 ? 0.8 : 0.75;
const scalesPadding = 0.5;
const scaleWidth =
_data.length * (CONSTANTS.barWidth + CONSTANTS.gap) + CONSTANTS.gap;
// Scales
const _x = x
.domain(_data.map(d => d.label))
.range([0, scaleWidth])
.paddingInner(innerPadding)
.paddingOuter(scalesPadding)
.round(true);
const _y = y.domain([0, Math.max(..._data.map(d => d.sum))]);
const yAxis = axisRight(_y)
.tickSize(width)
.tickFormat(d => yAxisTickFormat(d, defaultYAxisTickFormat(d)))
.ticks(4);
svg
.select(`[data-hook="${dataHooks.yAxis}"]`)
.call(yAxis)
.selectAll('.tick text')
.attr('x', -6)
.attr('dy', 4);
this.setState({
data: _data,
x: _x,
y: _y,
yAxis,
width,
});
};
componentDidMount() {
this.setState(
{
svg: select(this.chart.current),
},
this._update,
);
}
componentDidUpdate(prevProps) {
if (prevProps.data !== this.props.data) {
this.setState(
{
svg: select(this.chart.current),
},
this._update,
);
return true;
}
}
render() {
const { tooltipTemplate, tooltipCommonProps, className, dataHook } =
this.props;
const { data, x, y, width } = this.state;
return (
<div data-hook={dataHook} className={st(classes.root, {}, className)}>
{/* Chart */}
<svg ref={this.chart} width={width} height={this.height}>
{/* Axis */}
<g
data-hook={dataHooks.yAxis}
className={classes.yAxis}
transform={`translate(${this.margin.left}, ${this.margin.top})`}
/>
{/* Bars */}
<g
data-hook={dataHooks.bars}
transform={`translate(${this.margin.left}, ${this.margin.top})`}
>
{data.map((d, itemIndex) => {
let stacked = this.chartHeight;
return (
<g key={itemIndex}>
{d.values.map((value, index) => {
const height = this.chartHeight - y(value);
stacked -= height;
return (
<rect
key={index}
fill={this.colors[index]}
height={height}
width={CONSTANTS.barWidth}
x={x(d.label) - 25}
y={stacked}
/>
);
})}
</g>
);
})}
</g>
</svg>
{data &&
data.map((d, index) => (
<div
key={index}
className={classes.textPosition}
style={{
left: x(d.label) + this.margin.left - 25,
top: this.height - 15,
}}
>
<Text
textAlign="center"
skin="standard"
light
secondary
weight="normal"
size="small"
ellipsis
data-hook={dataHooks.xAxis}
>
{d.label}
</Text>
</div>
))}
{/* Tooltips */}
{tooltipTemplate &&
data.map((d, index) => (
<div
key={index}
className={classes.tooltip}
style={{
left: x(d.label) + this.margin.left - 25,
top: y(d.sum) + this.margin.top,
}}
>
<Tooltip
{...tooltipCommonProps}
content={tooltipTemplate(d)}
dataHook={dataHooks.tooltip}
>
<button
className={classes.tooltipInner}
style={{ height: `${this.chartHeight - y(d.sum)}px` }}
/>
</Tooltip>
</div>
))}
</div>
);
}
}
StackedBarChart.displayName = 'StackedBarChart';
StackedBarChart.propTypes = {
/** Applied as data-hook HTML attribute that can be used in the tests */
dataHook: PropTypes.string,
/** A css class to be applied to the component's root element */
className: PropTypes.string,
/** Chart data */
data: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
values: PropTypes.arrayOf(PropTypes.number),
}),
),
/** Tooltip template function */
tooltipTemplate: PropTypes.func,
/** Props that modify the Tooltip created from text bar charts */
tooltipCommonProps: PropTypes.shape(TooltipCommonProps),
/** Chart height (px) */
height: PropTypes.number,
/** Chart width (px) */
width: PropTypes.number,
/** Margin (px) for each side of the Chart. For example, in order to render larger number of digits at the yAxis, increase the left margin prop. */
margin: PropTypes.shape({
right: PropTypes.number,
left: PropTypes.number,
bottom: PropTypes.number,
top: PropTypes.number,
}),
/**
* ##### Formats Y axis ticks
* * `param` {string} `rawValue` - a raw value e.g. 10000
* * `param` {string} `rawValue` - number formatted value, comma separated e.g. 10,000
* * `return` {string} - the value to be shown as Y axis tick
*/
yAxisTickFormat: PropTypes.func,
};
StackedBarChart.defaultProps = {
data: [],
width: 900,
height: 350,
margin: CONSTANTS,
tooltipCommonProps: {},
yAxisTickFormat: defaultYAxisTickFormat,
};
export default StackedBarChart;