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