import React, { CSSProperties, Fragment,useEffect,useRef,useState } from ‘react‘ import { formatAmount } from ‘@/utils‘ const defaultColorList = [ [‘#5B8FF9‘, ‘#19A576‘], [‘#5B8FF9‘, ‘#42C090‘], [‘#9AC5FF‘, ‘#61DDAA‘], [‘#B8E1FF‘, ‘#9DF5CA‘], [‘pink‘, ‘yellow‘], ] const defaultLadderColor=[‘rgba(91,143,249,0.1)‘,‘rgba(66,192,144,0.1)‘] interface item { name: string numLeft: number rateLeft: number numRight: number rateRight: number } interface option { data: item[] nameLeft: string nameRight: string ladderColor?:string[] colorList?: string[][] } interface propsT { option: option className?: string } interface itemResult { name: string num: number rate: number maxVal: number maxValBoth: number } const BAR_HEIGHT = 40 const GAP_HEIGHT = 20 const LINE_GAP = 4 const POS_RATE = 0.7 const styleContainer: CSSProperties = { position: ‘relative‘, display: ‘flex‘, justifyContent: ‘center‘, alignItems: ‘flex-start‘, padding: ‘0 24px‘, marginTop: 14, } const styleAxisY: CSSProperties = { position: ‘relative‘, width: 40, display: ‘flex‘, justifyContent: ‘center‘, alignItems: ‘center‘, flexDirection: ‘column‘, zIndex:5 } const styleLabel: CSSProperties = { position: ‘relative‘, height: BAR_HEIGHT, width: 40, display: ‘flex‘, justifyContent: ‘center‘, alignItems: ‘center‘, backgroundColor: ‘#F5F5F5‘, color: ‘#666666‘, fontSize: 12, } const styleGap: CSSProperties = { position: ‘relative‘, display: ‘flex‘, justifyContent: ‘center‘, alignItems: ‘center‘, color: ‘#E5E5E5‘, fontSize: 12, height: GAP_HEIGHT, width: 18, } const styleLadder: CSSProperties = { position: ‘relative‘, backgroundColor: ‘rgba(66,192,144,0.1)‘, // opacity: 0.1, height: GAP_HEIGHT, width: ‘100%‘, } const styleWing: CSSProperties = { position: ‘relative‘, display: ‘flex‘, justifyContent: ‘center‘, // alignItems: ‘center‘, alignItems: ‘flex-start‘, flexDirection: ‘column‘, flexGrow: 1, zIndex:2, } const styleBar: CSSProperties = { position: ‘relative‘, display: ‘flex‘, justifyContent: ‘flex-start‘, alignItems: ‘center‘, height: BAR_HEIGHT, width: ‘100%‘, zIndex:5, } const styleRate: CSSProperties = { height: ‘100%‘, width: 0, backgroundColor: ‘#42C090‘, position: ‘relative‘, transition: ‘width .4s ease-in-out‘, } const styleNum: CSSProperties = { fontFamily: ‘AlibabaSans102-Bold‘, fontSize: 14, color: ‘#333333‘, position: ‘absolute‘, backgroundColor: ‘white‘, padding: ‘0 4px‘, height: ‘100%‘, display: ‘flex‘, justifyContent: ‘center‘, alignItems: ‘center‘, } const styleLineNum: CSSProperties = { fontFamily: ‘AlibabaSans102-Bold‘, fontSize: 12, color: ‘#666666‘, position: ‘absolute‘, // backgroundColor: ‘white‘, padding: ‘0 4px‘, top: 40, width: 36, textAlign: ‘center‘, zIndex:5, } const styleLineNumFix: CSSProperties={ position:‘absolute‘, width:4, height:‘100%‘, top:0, backgroundColor:‘#ffffff‘, zIndex:1 } const styleLine: CSSProperties = { width: ‘100%‘, height: `calc(${BAR_HEIGHT} + ${GAP_HEIGHT})`, // height: 50, border: ‘solid 1px pink‘, position: ‘absolute‘, top: BAR_HEIGHT/2, left: 8, zIndex: 1, opacity: 0.5, } const styleTriIcon: CSSProperties = { color: ‘#42C090‘, opacity: 0.7, transform: ‘rotate(90deg) scale(0.6)‘, // position: ‘absolute‘, // top: BAR_HEIGHT/2, fontSize: 12, zIndex: 10, position:‘relative‘, } const styleLeged: CSSProperties = { display: ‘flex‘, justifyContent: ‘flex-start‘, alignItems: ‘center‘, } const styleLegedItem: CSSProperties = { display: ‘flex‘, justifyContent: ‘flex-start‘, alignItems: ‘center‘, fontSize: 12, color: ‘#666666‘, marginRight: 32, } const styleLegedIcon: CSSProperties = { height: 6, width: 6, backgroundColor: ‘#5B8FF9‘, marginRight: 8, } const angle=(diffW:number , height:number)=>{ return Math.atan( height/diffW ) / (Math.PI/180) } const lineHeight = (nth: any, last: any): number => { return nth === 0 || nth === last ? (BAR_HEIGHT + GAP_HEIGHT - LINE_GAP ) :( BAR_HEIGHT + GAP_HEIGHT - 2 * LINE_GAP) } const lineTop = (nth: number): number => { return nth === 0 ? BAR_HEIGHT/2 : (BAR_HEIGHT + GAP_HEIGHT) * nth + BAR_HEIGHT/2 + LINE_GAP } const reduceRate = (data: itemResult[], index: number): number => { const [ item ] = data let rtn = item.maxVal/item.maxValBoth rtn = data.reduce((pre, cur, idx) => { let total = pre if (idx <= index) { total = pre * cur.rate } return total }, rtn) return rtn } const dataDeal = (rawData: item[], type: string): itemResult[] => { const maxL = Math.max(...rawData.map(item=>+item.numLeft)) const maxR = Math.max(...rawData.map(item=>+item.numRight)) return rawData.map(item => { return { name: item.name, num: type === ‘left‘ ? item.numLeft : item.numRight, rate: type === ‘left‘ ? item.rateLeft : item.rateRight, maxValBoth: Math.max(maxL,maxR), maxVal: type === ‘left‘ ? maxL: maxR, } }) } const axisY = (data: itemResult[]) => { return ( <div style={{ ...styleAxisY }}> {data.map((item, i) => { return ( <Fragment key={item.name}> <div style={{ ...styleLabel }}> <span dangerouslySetInnerHTML={{ __html: item.name }} /> </div> {i !== data.length - 1 && <div style={{ ...styleGap }}>▼</div>} </Fragment> ) })} </div> ) } const WingR = (data: itemResult[], colorList: string[][],ladderColor:string[]) => { const [fullWidth,setFullWidth]=useState<number>(0) const wingRef = useRef(null) useEffect( ()=>{ if (wingRef) { const { clientWidth = 0 } = wingRef!.current setFullWidth(clientWidth) } },[fullWidth]) return ( <div style={{ ...styleWing }} ref={wingRef}> {data .filter((_, i) => i !== 0) .map((item, n) => { return ( <Fragment key={item.name}> <div style={{ ...styleLine, // borderLeft: ‘unset‘, borderColor: `${colorList[0][1]} ${colorList[0][1]} ${colorList[0][1]} transparent`, height: lineHeight(n, data.length - 2), top: lineTop(n), }} /> <div style={{ ...styleLineNum, right: -28, top: lineTop(n) + lineHeight(n, data.length - 2) / 2 - 7, }} > <span style={{zIndex:5,position:‘relative‘}}>{+(+item.rate * 100).toFixed(1)}%</span> <div style={{...styleLineNumFix,right:‘50%‘}} /> </div> </Fragment> ) })} {data.map((item, i) => { return ( <Fragment key={item.name}> {i !== 0 && ( <div style={{ ...styleLadder, // backgroundColor: colorList[0][1], // backgroundColor: ladderColor[1], // clipPath: `polygon(0% 0%, ${reduceRate(data, i - 1) * 100}% 0%, ${reduceRate( // data, // i, // ) * 100}% 100%,0% 100%)`, width: `${reduceRate(data,i-1) * 100}%` }} > <div style={{width:‘100%‘,height:‘100%‘,backgroundColor: ladderColor[1],}} /> <div style={{ width:`${((fullWidth *reduceRate(data,i-1))**2 +GAP_HEIGHT**2)**0.5}px`, height:‘100%‘, backgroundColor: ‘#ffffff‘, transformOrigin:‘right top‘, transform:`rotate(${-angle(fullWidth *( reduceRate(data,i-1) - reduceRate(data,i)),GAP_HEIGHT)}deg)`, position:‘absolute‘, top:0, right:0, // left:0, }} /> </div> )} <div style={{ ...styleBar }}> <div style={{ ...styleRate, backgroundColor: colorList[i][1], width: `${reduceRate(data, i) * 100}%`, }} > <div style={{ ...styleNum, backgroundColor: reduceRate(data, i) >= POS_RATE ? ‘transparent‘ : ‘#fff‘, right: 0, top: ‘50%‘, transform: reduceRate(data, i) < POS_RATE ? ‘translateX(100%) translateY(-50%)‘ : ‘translateY(-50%)‘, }} > <span>{formatAmount(item.num ||0)}</span> {i !== 0 && ( <span style={{ ...styleTriIcon, transform: ‘rotate(90deg) scale(0.6)‘, top: i === data.length - 1 ? 0 : -5, right: -8, }} > ▼ </span> )} </div> </div> </div> </Fragment> ) })} </div> ) } const WingL = (data: itemResult[], colorList: string[][],ladderColor:string[]) => { const [fullWidth,setFullWidth]=useState<number>(0) const wingRef = useRef(null) useEffect( ()=>{ if (wingRef) { const { clientWidth = 0 } = wingRef!.current setFullWidth(clientWidth) } },[fullWidth]) return ( <div style={{ ...styleWing ,alignItems:‘flex-end‘}} ref={wingRef}> {data .filter((_, i) => i !== 0) .map((item, n) => { return ( <Fragment key={item.name}> <div style={{ ...styleLine, borderColor: `${colorList[0][0]} transparent ${colorList[0][0]} ${colorList[0][0]}`, // borderRight: ‘unset‘, left: -8, height: lineHeight(n, data.length - 2), top: lineTop(n), }} /> <div style={{ ...styleLineNum, left: -28, top: lineTop(n) + lineHeight(n, data.length - 2) / 2 - 7, }} > <span style={{zIndex:5,position:‘relative‘}}>{+(+item.rate * 100).toFixed(1)}%</span> <div style={{...styleLineNumFix,left:‘50%‘,}} /> </div> </Fragment> ) })} {data.map((item, i) => { return ( <Fragment key={item.name}> {i !== 0 && ( <div style={{ ...styleLadder, // backgroundColor: colorList[0][0], backgroundColor: ladderColor[0], // clipPath: `polygon(${100 - // reduceRate(data, i - 1) * 100}% 0%, 100% 0%,100% 100%, ${100 - // reduceRate(data, i) * 100}% 100%)`, width: `${reduceRate(data,i-1) * 100}%` }} > <div style={{width:‘100%‘,height:‘100%‘,backgroundColor: ladderColor[0],}} /> <div style={{ width:`${((fullWidth *reduceRate(data,i-1))**2 +GAP_HEIGHT**2)**0.5}px`, height:‘100%‘, backgroundColor: ‘#ffffff‘, transformOrigin:‘left top‘, transform:`rotate(${angle(fullWidth *( reduceRate(data,i-1) - reduceRate(data,i)),GAP_HEIGHT)}deg)`, position:‘absolute‘, top:0, left:0, }} /> </div> )} <div style={{ ...styleBar, justifyContent: ‘flex-end‘ }}> <div style={{ ...styleRate, backgroundColor: colorList[i][0], width: `${reduceRate(data, i) * 100}%`, }} > <div style={{ ...styleNum, backgroundColor: reduceRate(data, i) >= POS_RATE ? ‘transparent‘ : ‘#fff‘, left: 0, top: ‘50%‘, transform: reduceRate(data, i) < POS_RATE ? ‘translateX(-100%) translateY(-50%)‘ : ‘translateY(-50%)‘, }} > {i !== 0 && ( <span style={{ ...styleTriIcon, color: colorList[0][0], transform: ‘rotate(30deg) scale(0.6)‘, top: i === data.length - 1 ? 0 : -4, left: -8, }} > ▼ </span> )} <span>{formatAmount(item.num||0)}</span> </div> </div> </div> </Fragment> ) })} </div> ) } const legend = (legendList: string[] = [], colorList: string[][]) => { return ( <div style={{ ...styleLeged }}> {legendList.map((item, i) => { return ( <div style={{ ...styleLegedItem }} key={item}> <span style={{ ...styleLegedIcon, backgroundColor: colorList[0][i] }} /> <span>{item}</span> </div> ) })} </div> ) } const FunnelChart = ({ option,className=‘‘ }: propsT) => { // console.log(‘option==‘, option) const { data, nameLeft, nameRight, colorList = defaultColorList,ladderColor=defaultLadderColor,} = option return ( <div className={`funnel-hart ${className}`}> {legend([nameLeft, nameRight], colorList)} <div style={{ ...styleContainer }}> {WingL(dataDeal(data, ‘left‘), colorList,ladderColor)} {axisY(dataDeal(data, ‘‘))} {WingR(dataDeal(data, ‘right‘), colorList,ladderColor)} </div> </div> ) } export default FunnelChart