UNPKG

wix-style-react

Version:
285 lines (247 loc) • 7.61 kB
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;