fileOverview import React , { PureComponent , ReactElement } from 'react' ; import Animate from 'react-smooth' ; import classNames from 'classnames' ; import _ from 'lodash' ; import { Curve , CurveType , Props as CurveProps , Point as CurvePoint } from '../shape/Curve' ; import { Dot , Props as DotProps } from '../shape/Dot' ; import { Layer } from '../container/Layer' ; import { LabelList } from '../component/LabelList' ; import { ErrorBar , Props as ErrorBarProps } from './ErrorBar' ; import { uniqueId , interpolateNumber } from '../util/DataUtils' ; import { findAllByType } from '../util/ReactUtils' ; import { Global } from '../util/Global' ; import { getCateCoordinateOfLine , getValueByDataKey } from '../util/ChartUtils' ; import { Props as XAxisProps } from './XAxis' ; import { Props as YAxisProps } from './YAxis' ; import { D3Scale , LegendType , TooltipType , AnimationTiming , filterProps , ChartOffset , DataKey , TickItem , } from '../util/types' ; type LineDot = ReactElement < SVGElement > | ( ( props : any ) => ReactElement < SVGElement > ) | DotProps | boolean ; interface LinePointItem extends CurvePoint { value ? : number ; payload ? : any ; } interface InternalLineProps { top ? : number ; left ? : number ; width ? : number ; height ? : number ; points ? : LinePointItem [ ] ; xAxis ? : Omit < XAxisProps , 'scale' > & { scale : D3Scale < string | number > ; } ; yAxis ? : Omit < YAxisProps , 'scale' > & { scale : D3Scale < string | number > ; } ; } interface LineProps extends InternalLineProps { className ? : string ; data ? : any ; type ? : CurveType ; unit ? : string | number ; name ? : string | number ; yAxisId ? : string | number ; xAxisId ? : string | number ; dataKey ? : DataKey < any > ; legendType ? : LegendType ; tooltipType ? : TooltipType ; layout ? : 'horizontal' | 'vertical' ; connectNulls ? : boolean ; hide ? : boolean ; activeDot ? : LineDot ; dot ? : LineDot ; onAnimationStart ? : ( ) => void ; onAnimationEnd ? : ( ) => void ; isAnimationActive ? : boolean ; animateNewValues ? : boolean ; animationBegin ? : number ; animationDuration ? : number ; animationEasing ? : AnimationTiming ; animationId ? : number ; id ? : string ; } export type Props = Omit < CurveProps , 'points' | 'pathRef' > & LineProps ; interface State { isAnimationFinished ? : boolean ; totalLength ? : number ; prevPoints ? : LinePointItem [ ] ; curPoints ? : LinePointItem [ ] ; prevAnimationId ? : number ; } export class Line extends PureComponent < Props , State > { static displayName = 'Line' ; static defaultProps = { xAxisId : 0 , yAxisId : 0 , connectNulls : false , activeDot : true , dot : true , legendType : 'line' , stroke : '#3182bd' , strokeWidth : 1 , fill : '#fff' , points : [ ] as LinePointItem [ ] , isAnimationActive : ! Global . isSsr , animateNewValues : true , animationBegin : 0 , animationDuration : 1500 , animationEasing : 'ease' , hide : false , } ; param { Object } props param { Object } xAxis param { Object } yAxis param { String } dataKey return { Array } static getComposedData = ( { props , xAxis , yAxis , xAxisTicks , yAxisTicks , dataKey , bandSize , displayedData , offset , } ) => { const { layout } = props ; const points = displayedData . map ( ( entry , index ) => { const value = getValueByDataKey ( entry , dataKey ) ; if ( layout === 'horizontal' ) { return { x : getCateCoordinateOfLine ( { axis : xAxis , ticks : xAxisTicks , bandSize , entry , index } ) , y : _ . isNil ( value ) ? null : yAxis . scale ( value ) , value , payload : entry , } ; } return { x : _ . isNil ( value ) ? null : xAxis . scale ( value ) , y : getCateCoordinateOfLine ( { axis : yAxis , ticks : yAxisTicks , bandSize , entry , index } ) , value , payload : entry , } ; } ) ; return { points , layout , ... offset } ; } ; mainCurve ? : SVGPathElement ; state : State = { isAnimationFinished : true , totalLength : 0 , } ; componentDidMount ( ) { if ( ! this . props . isAnimationActive ) { return ; } const totalLength = this . getTotalLength ( ) ; this . setState ( { totalLength } ) ; } static getDerivedStateFromProps ( nextProps , prevState ) { if ( nextProps . animationId !== prevState . prevAnimationId ) { return { prevAnimationId : nextProps . animationId , curPoints : nextProps . points , prevPoints : prevState . curPoints , } ; } if ( nextProps . points !== prevState . curPoints ) { return { curPoints : nextProps . points , } ; } return null ; } getTotalLength ( ) { const curveDom = this . mainCurve ; try { return ( curveDom && curveDom . getTotalLength && curveDom . getTotalLength ( ) ) || 0 ; } catch ( err ) { return 0 ; } } getStrokeDasharray = ( length , totalLength , lines ) => { const lineLength = lines . reduce ( ( pre , next ) => pre + next ) ; const count = Math . floor ( length / lineLength ) ; const remainLength = length % lineLength ; const restLength = totalLength - length ; let remainLines = [ ] ; for ( let i = 0 , sum = 0 ; ; sum += lines [ i ] , ++ i ) { if ( sum + lines [ i ] > remainLength ) { remainLines = [ ... lines . slice ( 0 , i ) , remainLength - sum ] ; break ; } } const emptyLines = remainLines . length % 2 === 0 ? [ 0 , restLength ] : [ restLength ] ; return [ ... Line . repeat ( lines , count ) , ... remainLines , ... emptyLines ] . map ( line => `${ line }px` ) . join ( ', ' ) ; } ; id = uniqueId ( 'recharts-line-' ) ; pathRef = ( node ) : void => { this . mainCurve = node ; } ; static repeat ( lines , count ) { const linesUnit = lines . length % 2 !== 0 ? [ ... lines , 0 ] : lines ; let result = [ ] ; for ( let i = 0 ; i < count ; ++ i ) { result = [ ... result , ... linesUnit ] ; } return result ; } handleAnimationEnd = ( ) => { this . setState ( { isAnimationFinished : true } ) ; if ( this . props . onAnimationEnd ) { this . props . onAnimationEnd ( ) ; } } ; handleAnimationStart = ( ) => { this . setState ( { isAnimationFinished : false } ) ; if ( this . props . onAnimationStart ) { this . props . onAnimationStart ( ) ; } } ; renderErrorBar ( ) { if ( this . props . isAnimationActive && ! this . state . isAnimationFinished ) { return null ; } const { points , xAxis , yAxis , layout , children } = this . props ; const errorBarItems = findAllByType ( children , ErrorBar . displayName ) ; if ( ! errorBarItems ) { return null ; } function dataPointFormatter ( dataPoint , dataKey ) { return { x : dataPoint . x , y : dataPoint . y , value : dataPoint . value , errorVal : getValueByDataKey ( dataPoint . payload , dataKey ) , } ; } return errorBarItems . map ( ( item , i ) => React . cloneElement ( item , { key : `bar-${ i }` , data : points , xAxis , yAxis , layout , dataPointFormatter , } ) ) ; } static renderDotItem ( option , props ) { let dotItem ; if ( React . isValidElement ( option ) ) { dotItem = React . cloneElement ( option , props ) ; } else if ( _ . isFunction ( option ) ) { dotItem = option ( props ) ; } else { const className = classNames ( 'recharts-line-dot' , option ? ( option as DotProps ) . className : '' ) ; dotItem = < Dot { ... props } className = { className } / > ; } return dotItem ; } renderDots ( needClip , clipPathId ) { const { isAnimationActive } = this . props ; if ( isAnimationActive && ! this . state . isAnimationFinished ) { return null ; } const { dot , points , dataKey } = this . props ; const lineProps = filterProps ( this . props ) ; const customDotProps = filterProps ( dot , true ) ; const dots = points . map ( ( entry , i ) => { const dotProps = { key : `dot-${ i }` , r : 3 , ... lineProps , ... customDotProps , value : entry . value , dataKey , cx : entry . x , cy : entry . y , index : i , payload : entry . payload , } ; return Line . renderDotItem ( dot , dotProps ) ; } ) ; const dotsProps = { clipPath : needClip ? `url(#clipPath-${ clipPathId })` : null , } ; return ( < Layer className = "recharts-line-dots" key = "dots" { ... dotsProps } > { dots } < /Layer>); } renderCurveStatically ( points , needClip , clipPathId , props ? ) { const { type , layout , connectNulls , ref , ... others } = this . props ; const curveProps = { ... filterProps ( others , true ) , fill : 'none' , className : 'recharts-line-curve' , clipPath : needClip ? `url(#clipPath-${ clipPathId })` : null , points , ... props , type , layout , connectNulls , } ; return < Curve { ... curveProps } pathRef = { this . pathRef } / > ; } renderCurveWithAnimation ( needClip , clipPathId ) { const { points , strokeDasharray , isAnimationActive , animationBegin , animationDuration , animationEasing , animationId , animateNewValues , width , height , } = this . props ; const { prevPoints , totalLength } = this . state ; return ( < Animate begin = { animationBegin } duration = { animationDuration } isActive = { isAnimationActive } easing = { animationEasing } from = { { t : 0 } } to = { { t : 1 } } key = { `line-${ animationId }` } onAnimationEnd = { this . handleAnimationEnd } onAnimationStart = { this . handleAnimationStart } > { ( { t } ) => { if ( prevPoints ) { const prevPointsDiffFactor = prevPoints . length / points . length ; const stepData = points . map ( ( entry , index ) => { const prevPointIndex = Math . floor ( index * prevPointsDiffFactor ) ; if ( prevPoints [ prevPointIndex ] ) { const prev = prevPoints [ prevPointIndex ] ; const interpolatorX = interpolateNumber ( prev . x , entry . x ) ; const interpolatorY = interpolateNumber ( prev . y , entry . y ) ; return { ... entry , x : interpolatorX ( t ) , y : interpolatorY ( t ) } ; } if ( animateNewValues ) { const interpolatorX = interpolateNumber ( width * 2 , entry . x ) ; const interpolatorY = interpolateNumber ( height / 2 , entry . y ) ; return { ... entry , x : interpolatorX ( t ) , y : interpolatorY ( t ) } ; } return { ... entry , x : entry . x , y : entry . y } ; } ) ; return this . renderCurveStatically ( stepData , needClip , clipPathId ) ; } const interpolator = interpolateNumber ( 0 , totalLength ) ; const curLength = interpolator ( t ) ; let currentStrokeDasharray ; if ( strokeDasharray ) { const lines = `${ strokeDasharray }` . split ( /[,\s]+/gim ) . map ( num => parseFloat ( num ) ) ; currentStrokeDasharray = this . getStrokeDasharray ( curLength , totalLength , lines ) ; } else { currentStrokeDasharray = `${ curLength }px ${ totalLength - curLength }px` ; } return this . renderCurveStatically ( points , needClip , clipPathId , { strokeDasharray : currentStrokeDasharray , } ) ; } } < /Animate>); } renderCurve ( needClip , clipPathId ) { const { points , isAnimationActive } = this . props ; const { prevPoints , totalLength } = this . state ; if ( isAnimationActive && points && points . length && ( ( ! prevPoints && totalLength > 0 ) || ! _ . isEqual ( prevPoints , points ) ) ) { return this . renderCurveWithAnimation ( needClip , clipPathId ) ; } return this . renderCurveStatically ( points , needClip , clipPathId ) ; } render ( ) { const { hide , dot , points , className , xAxis , yAxis , top , left , width , height , isAnimationActive , id } = this . props ; if ( hide || ! points || ! points . length ) { return null ; } const { isAnimationFinished } = this . state ; const hasSinglePoint = points . length === 1 ; const layerClass = classNames ( 'recharts-line' , className ) ; const needClip = ( xAxis && xAxis . allowDataOverflow ) || ( yAxis && yAxis . allowDataOverflow ) ; const clipPathId = _ . isNil ( id ) ? this . id : id ; return ( < Layer className = { layerClass } > { needClip ? ( < defs > < clipPath id = { `clipPath-${ clipPathId }` } > < rect x = { left } y = { top } width = { width } height = { height } / > < /clipPath> < /defs>) : null} { ! hasSinglePoint && this . renderCurve ( needClip , clipPathId ) } { this . renderErrorBar ( ) } { ( hasSinglePoint || dot ) && this . renderDots ( needClip , clipPathId ) } { ( ! isAnimationActive || isAnimationFinished ) && LabelList . renderCallByParent ( this . props , points ) } < /Layer>); } }