div 实现漏斗图

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

  

div 实现漏斗图

上一篇:QMI8658C惯性模块开发记录


下一篇:题解 CF1550C. Manhattan Subarrays (思维)