/** * @fileOverview Render a group of radial bar */ import React, { PureComponent, ReactElement, ReactNode } from 'react'; import classNames from 'classnames'; import Animate from 'react-smooth'; import _ from 'lodash'; import { Sector, Props as SectorProps } from '../shape/Sector'; import { Layer } from '../container/Layer'; import { findAllByType } from '../util/ReactUtils'; import { Global } from '../util/Global'; import { ImplicitLabelListType, LabelList } from '../component/LabelList'; import { Cell } from '../component/Cell'; import { mathSign, interpolateNumber } from '../util/DataUtils'; import { getCateCoordinateOfBar, findPositionOfBar, getValueByDataKey, truncateByDomain, getBaseValueOfBar, getTooltipItem, } from '../util/ChartUtils'; import { LegendType, TooltipType, AnimationTiming, filterProps, TickItem, adaptEventsOfChild, PresentationAttributesAdaptChildEvent, } from '../util/types'; import { polarToCartesian } from '../util/PolarUtils'; // TODO: Cause of circular dependency. Needs refactoring of functions that need them. // import { AngleAxisProps, RadiusAxisProps } from './types'; type RadialBarDataItem = SectorProps & { value?: any; payload?: any; background?: SectorProps; }; type RadialBarShape = ReactElement | ((props: Props) => ReactNode); type RadialBarBackground = ReactElement | ((props: Props) => ReactNode) | SectorProps | boolean; interface RadialBarProps { animationId?: string | number; className?: string; angleAxisId?: string | number; radiusAxisId?: string | number; startAngle?: number; endAngle?: number; shape?: RadialBarShape; activeShape?: RadialBarShape; activeIndex?: number; dataKey: string | number | ((obj: any) => any); cornerRadius?: string | number; forceCornerRadius?: boolean; cornerIsExternal?: boolean; minPointSize?: number; maxBarSize?: number; data?: RadialBarDataItem[]; legendType?: LegendType; tooltipType?: TooltipType; hide?: boolean; label?: ImplicitLabelListType; stackId?: string | number; background?: RadialBarBackground; onAnimationStart?: () => void; onAnimationEnd?: () => void; isAnimationActive?: boolean; animationBegin?: number; animationDuration?: number; animationEasing?: AnimationTiming; } export type Props = PresentationAttributesAdaptChildEvent & RadialBarProps; interface State { readonly isAnimationFinished?: boolean; readonly prevData?: RadialBarDataItem[]; readonly curData?: RadialBarDataItem[]; readonly prevAnimationId?: string | number; } export class RadialBar extends PureComponent { static displayName = 'RadialBar'; static defaultProps = { angleAxisId: 0, radiusAxisId: 0, minPointSize: 0, hide: false, legendType: 'rect', data: [] as RadialBarDataItem[], isAnimationActive: !Global.isSsr, animationBegin: 0, animationDuration: 1500, animationEasing: 'ease', forceCornerRadius: false, cornerIsExternal: false, }; static getComposedData = ({ item, props, radiusAxis, radiusAxisTicks, angleAxis, angleAxisTicks, displayedData, dataKey, stackedData, barPosition, bandSize, dataStartIndex, }: { item: RadialBar; props: any; radiusAxis: any; // RadiusAxisProps; radiusAxisTicks: Array; angleAxis: any; // AngleAxisProps; angleAxisTicks: Array; displayedData: any[]; dataKey: Props['dataKey']; stackedData?: any[]; barPosition?: any[]; bandSize?: number; dataStartIndex: number; }) => { const pos = findPositionOfBar(barPosition, item); if (!pos) { return null; } const { cx, cy } = angleAxis; const { layout } = props; const { children, minPointSize } = item.props; const numericAxis = layout === 'radial' ? angleAxis : radiusAxis; const stackedDomain = stackedData ? numericAxis.scale.domain() : null; const baseValue = getBaseValueOfBar({ numericAxis }); const cells = findAllByType(children, Cell.displayName); const sectors = displayedData.map((entry: any, index: number) => { let value, innerRadius, outerRadius, startAngle, endAngle, backgroundSector; if (stackedData) { value = truncateByDomain(stackedData[dataStartIndex + index], stackedDomain); } else { value = getValueByDataKey(entry, dataKey); if (!_.isArray(value)) { value = [baseValue, value]; } } if (layout === 'radial') { innerRadius = getCateCoordinateOfBar({ axis: radiusAxis, ticks: radiusAxisTicks, bandSize, offset: pos.offset, entry, index, }); endAngle = angleAxis.scale(value[1]); startAngle = angleAxis.scale(value[0]); outerRadius = innerRadius + pos.size; const deltaAngle = endAngle - startAngle; if (Math.abs(minPointSize) > 0 && Math.abs(deltaAngle) < Math.abs(minPointSize)) { const delta = mathSign(deltaAngle || minPointSize) * (Math.abs(minPointSize) - Math.abs(deltaAngle)); endAngle += delta; } backgroundSector = { background: { cx, cy, innerRadius, outerRadius, startAngle: props.startAngle, endAngle: props.endAngle, }, }; } else { innerRadius = radiusAxis.scale(value[0]); outerRadius = radiusAxis.scale(value[1]); startAngle = getCateCoordinateOfBar({ axis: angleAxis, ticks: angleAxisTicks, bandSize, offset: pos.offset, entry, index, }); endAngle = startAngle + pos.size; const deltaRadius = outerRadius - innerRadius; if (Math.abs(minPointSize) > 0 && Math.abs(deltaRadius) < Math.abs(minPointSize)) { const delta = mathSign(deltaRadius || minPointSize) * (Math.abs(minPointSize) - Math.abs(deltaRadius)); outerRadius += delta; } } return { ...entry, ...backgroundSector, payload: entry, value: stackedData ? value : value[1], cx, cy, innerRadius, outerRadius, startAngle, endAngle, ...(cells && cells[index] && cells[index].props), tooltipPayload: [getTooltipItem(item, entry)], tooltipPosition: polarToCartesian(cx, cy, (innerRadius + outerRadius) / 2, (startAngle + endAngle) / 2), }; }); return { data: sectors, layout }; }; state: State = { isAnimationFinished: false, }; static getDerivedStateFromProps(nextProps: Props, prevState: State): State { if (nextProps.animationId !== prevState.prevAnimationId) { return { prevAnimationId: nextProps.animationId, curData: nextProps.data, prevData: prevState.curData, }; } if (nextProps.data !== prevState.curData) { return { curData: nextProps.data, }; } return null; } getDeltaAngle() { const { startAngle, endAngle } = this.props; const sign = mathSign(endAngle - startAngle); const deltaAngle = Math.min(Math.abs(endAngle - startAngle), 360); return sign * deltaAngle; } handleAnimationEnd = () => { const { onAnimationEnd } = this.props; this.setState({ isAnimationFinished: true }); if (_.isFunction(onAnimationEnd)) { onAnimationEnd(); } }; handleAnimationStart = () => { const { onAnimationStart } = this.props; this.setState({ isAnimationFinished: false }); if (_.isFunction(onAnimationStart)) { onAnimationStart(); } }; static renderSectorShape(shape: RadialBarBackground, props: any) { let sectorShape; if (React.isValidElement(shape)) { sectorShape = React.cloneElement(shape, props); } else if (_.isFunction(shape)) { sectorShape = shape(props); } else { sectorShape = React.createElement(Sector, props); } return sectorShape; } renderSectorsStatically(sectors: SectorProps[]) { const { shape, activeShape, activeIndex, cornerRadius, ...others } = this.props; const baseProps = filterProps(others); return sectors.map((entry, i) => { const props = { ...baseProps, cornerRadius, ...entry, ...adaptEventsOfChild(this.props, entry, i), key: `sector-${i}`, className: 'recharts-radial-bar-sector', forceCornerRadius: others.forceCornerRadius, cornerIsExternal: others.cornerIsExternal, }; return RadialBar.renderSectorShape(i === activeIndex ? activeShape : shape, props); }); } renderSectorsWithAnimation() { const { data, isAnimationActive, animationBegin, animationDuration, animationEasing, animationId } = this.props; const { prevData } = this.state; return ( {({ t }: { t: number }) => { const stepData = data.map((entry, index) => { const prev = prevData && prevData[index]; if (prev) { const interpolatorStartAngle = interpolateNumber(prev.startAngle, entry.startAngle); const interpolatorEndAngle = interpolateNumber(prev.endAngle, entry.endAngle); return { ...entry, startAngle: interpolatorStartAngle(t), endAngle: interpolatorEndAngle(t), }; } const { endAngle, startAngle } = entry; const interpolator = interpolateNumber(startAngle, endAngle); return { ...entry, endAngle: interpolator(t) }; }); return {this.renderSectorsStatically(stepData)}; }} ); } renderSectors() { const { data, isAnimationActive } = this.props; const { prevData } = this.state; if (isAnimationActive && data && data.length && (!prevData || !_.isEqual(prevData, data))) { return this.renderSectorsWithAnimation(); } return this.renderSectorsStatically(data); } renderBackground(sectors?: RadialBarDataItem[]) { const { cornerRadius } = this.props; const backgroundProps = filterProps(this.props.background); return sectors.map((entry, i) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { value, background, ...rest } = entry; if (!background) { return null; } const props = { cornerRadius, ...rest, fill: '#eee', ...background, ...backgroundProps, ...adaptEventsOfChild(this.props, entry, i), index: i, key: `sector-${i}`, className: 'recharts-radial-bar-background-sector', }; return RadialBar.renderSectorShape(background, props); }); } render() { const { hide, data, className, background, isAnimationActive } = this.props; if (hide || !data || !data.length) { return null; } const { isAnimationFinished } = this.state; const layerClass = classNames('recharts-area', className); return ( {background && {this.renderBackground(data)}} {this.renderSectors()} {(!isAnimationActive || isAnimationFinished) && LabelList.renderCallByParent( { ...this.props, clockWise: this.getDeltaAngle() < 0, }, data, )} ); } }