likes
comments
collection
share

Antd Charts 组织架构图 && 指标拆解图

作者站长头像
站长
· 阅读数 9

Antd Charts 组织架构图 && 指标拆解图

-   开箱即用:默认呈现高质量图表,将对开发体验及用户体验的研究沉淀入图表的默认配置项
-   易于配置:用户能够根据具体业务需要较为轻松的调整图表细节
-   体验良好:视觉和交互体验聚焦于如何能够**展示和发现信息**"这一图表本源的职能上

它的文档是真的少。。。。。需要自己花时间研究。。。。。。。
1.根据数据层级关系,以树级的形式展开
2.支持一键展开,一键收起;
3.根据自己的UI,个性化设计

...类似于下面这种需求

Antd Charts 组织架构图 && 指标拆解图

存在的问题

1.整个图表,根据你传输的数据进行渲染

2.每个节点的折叠与展开

3.每个节点的样式自定义问题

4.特定的一键展开与一键收起

  1. 即使我们选择 edgeCfg: { type: 'polyline' } , 有可能内容过多,无法达到每一级连接线,保证九十度连接;原因(节点配置 nodeCfg 开启autoWidth 宽度自适应)

下面我们一个个来解决

核心API简化

  • 节点无法收起与展开

Antd Charts 组织架构图 && 指标拆解图

markerCfg:{
        show: true,
    },
markerCfg: (cfg) => {
      return {
        position: 'right',
        show: cfg.children?.length,
        style: (arg) => {
          return {
            stroke: arg.value.percent > 0.3 ? stroke : '#1f8fff',
          };
        },
      };
    },
  • 节点自定义---一般是避免不了!!!

支持单节点添加多内容,不止三个,通过group!.addShape('text', {...})

customContent: (item, group, cfg) => {
    const { startX, startY, width } = cfg;
    const { text, value, icon } = item;
    let textShape, valueShape, iconShape; // 这里不止添加三个!!!
    
    textShape =
      text &&
      group!.addShape('text', {
        attrs: {
          textBaseline: 'top',
          x: startX,
          y: startY,
          text,
          fill: '#aaa',
        },
        // group 内唯一字段
        name: `text-${Math.random()}`,
      });
    valueShape =
      value &&
      group!.addShape('text', {
        attrs: {
          textBaseline: 'top',
          x: startX + width / 2,
          y: startY,
          text: value,
          fill: '#f00',
        },
        name: `value-${Math.random()}`,
      });
    iconShape =
      icon &&
      group!.addShape('image', {
        attrs: {
          x: startX,
          y: startY,
          width: 72,
          height: 72,
          img: icon,
        },
        name: `image-${Math.random()}`,
      });
    return Math.max(
      textShape?.getBBox().height ?? 0,
      valueShape?.getBBox().height ?? 0,
      iconShape?.getBBox().height ?? 0,
    );
  },
##### 数据的转化

```后端数据格式
const fecth = {
    data:{
        id: '1',
        type: 0,
        count: 15,
        childList: [
            {},
            {},
            {}
        ]
        ....
    }
}
 // 按照组件类库的数据格式
 // 下面以指标拆解图的格式为例
 const { data = {} } = await fetchData()
 const handleTransformData = ( arr = [] ) => {
    const finnallyData =  arr?.map(item=>({
        // 我们想要的数据格式· 其实递归 === 套公式
        // 结构出来最美观、方便了...
        id: item?.id,
        value: {
        title: item?.nodeName,
            items: [
                {
                    type: item?.type,
                    count: item?.count
                }
            ]
        },
        children: handleTransformData(item?.childList) || null, // 核心一步
    }))
    
    return finnallyData
 }
  • 节点的配置
nodeCfg: {
      size: [140, 25],
      percent: {
        position: 'bottom',
        size: 4,
        style: (arg) => {
          return {
            radius: [0, 0, 0, 2],
            fill: arg.value.percent > 0.3 ? stroke : '#1f8fff',
          };
        },
      },
      items: {
        containerStyle: {
          fill: '#fff',
        },
        padding: 6,
        style: (cfg, group, type) => {
          const styles = {
            icon: {
              width: 12,
              height: 12,
            },
            value: {
              fill: '#f00',
            },
            text: {
              fill: '#aaa',
            },
          };
          return styles[type];
        },
      },
      nodeStateStyles: {
        hover: {
          lineWidth: 2,
        },
      },
      title: {
        containerStyle: {
          fill: 'transparent',
        },
        style: {
          fill: '#000',
          fontSize: 12,
        },
      },
      style: (arg) => {
        return {
          fill: '#fff',
          radius: 2,
          stroke: arg.value.percent > 0.3 ? stroke : '#1f8fff',
        };
      },
    },
  • 节点的起始点

Antd Charts 组织架构图 && 指标拆解图

  • 边的配置
edgeCfg: {
      label: {
        style: {
          fill: '#aaa',
          fontSize: 12,
          fillOpacity: 1,
        },
      },
      style: (edge) => {
        return {
          stroke: '#518AD3',
          strokeOpacity: 0.5,
        };
      },
      endArrow: {
        fill: '#518AD3',
      },
      edgeStateStyles: {
        hover: {
          strokeOpacity: 1,
        },
      },
    },
  • 交互方式 根据意思:画布拖拽,节点拖拽,zoom-canvas我也不知道。。。
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-node'],
  • 节点连接线 保持 90°直角
// 边配置
 edgeCfg: {
      type: 'polyline',
    },
    
// 节点配置
 nodeCfg: {
      // 节点连接点
      anchorPoints: [
        [0.5, 0.0],
        [0.5, 1],
      ],
      // 固定尺寸
      size: [250, 50],
      // 取消自动宽度
      autoWidth: false,
      },
   
  // 布局配置
   layout: {
       // 可以理解为纵向
      direction: 'TB',
      // 每一级节点之间的垂直间距
      getVGap: () => {
        return 45
      },
      // 每一级节点之间的水平间距
      getHGap: () => {
        return 180
      },
    },
  
 // 根据文本动态设定间距 举个栗子
  const handleTextDisplayTrendCalc = (text, folderType, isReply, totalSchedule) => {
    const isScheduleOrSubject = folderType === FolderType.Schedule || folderType === FolderType.Subject
    const isStage = folderType === FolderType.Stage
    const isProject = folderType === FolderType.Project
    const graphTextRender = () => {
      if (folderType === FolderType.Subject) {
        return totalSchedule
      }
      return ''
    }
    const displayValue = graphTextRender()
    const valueMarginLeft = `${isScheduleOrSubject ? `${displayValue}${text}` : text}`.length + 5
    const isHaveApprovalIcon = isReply ? valueMarginLeft + 2 : valueMarginLeft
    console.log('`${text}`.length', `${text}`.length)

    // 15个为界限
    if (text && `${text}`.length >= 15) {
      const disText = `${text.slice(0, 12)}...`

      return {
        xText: 3.5,
        xStatusIcon: 1,
        xApproval: 8.5,
        xValue: isHaveApprovalIcon,
        label: disText,
      }
    }
    // 10 - 15
    if (text && `${text}`.length < 15 && `${text}`.length > 10) {
      return {
        xText: 3.5,
        xStatusIcon: 1,
        xApproval: 8.5,
        xValue: isHaveApprovalIcon,
        label: text,
      }
    }
    // 8 -10
    if (text && `${text}`.length <= 10 && `${text}`.length >= 8) {
      return {
        xText: 5.5,
        xStatusIcon: 3.5,
        xApproval: 8.5,
        xValue: isHaveApprovalIcon + 2.5,
        label: text,
      }
    }
    // 5 - 8
    if (text && `${text}`.length < 8 && `${text}`.length >= 5) {
      return {
        xText: 7.5,
        xStatusIcon: 5.2,
        xApproval: 17.5,
        xValue: isHaveApprovalIcon + 5.5,
        label: text,
      }
    }
    // 0 - 5
    if (text && `${text}`.length < 5) {
      return {
        xText: 9.5,
        xStatusIcon: 7.2,
        xApproval: 15.5,
        xValue: isHaveApprovalIcon + 5.5,
        label: text,
      }
    }

    return {
      xText: 3.5,
      xStatusIcon: 1,
      xApproval: 8.5,
      xValue: isHaveApprovalIcon,
      label: text,
    }
  }
 // 节点 item 自定义配置
   customContent: (item, group, cfg) => {
        const { startX, startY } = cfg
        const { type, name: text, status, stageNum = 0, isReply, totalSchedule } = item
        // 使用文本动态设定间距 举个栗子
        const { xText, xStatusIcon, xApproval, xValue, label } = handleTextDisplayTrendCalc(
          text,
          type,
          isReply,
          totalSchedule
        )

        console.log(xText, xStatusIcon, xApproval, xValue, label)

        const isScheduleOrSubject = type === FolderType.Schedule || type === FolderType.Subject
        const isStage = type === FolderType.Stage
        const isProject = type === FolderType.Project
        let textShape
        let valueShape
        let statusIconShape
        let approvalIconShape
        // eslint-disable-next-line prefer-const
        textShape =
          text &&
          group!.addShape('text', {
            attrs: {
              textBaseline: 'top',
              // eslint-disable-next-line no-unsafe-optional-chaining
              x: xText * startX,
              y: 2.5 * startY,
              text: label,
              fill: '#1F1F1F',
              fontWeight: 700,
              fontSize: 14,
            },
            // group 内唯一字段
            name: `text-${Math.random()}`,
          })
        const displayValue = graphNodeValueRender(item)
        const valueMarginLeft = `${isScheduleOrSubject ? `${displayValue}${text}` : text}`.length + 5
        const isHaveApprovalIcon = !isReply ? valueMarginLeft + 2 : valueMarginLeft
        // eslint-disable-next-line prefer-const
        valueShape = group!.addShape('text', {
          attrs: {
            textBaseline: 'top',
            x: xValue * startX,
            y: 2.5 * startY,
            text: displayValue,
            fill: '#1F1F1F',
            fontWeight: 700,
            fontSize: 14,
          },
          name: `value-${Math.random()}`,
        })
        const handleGetNodeIcon = (statusVal, stageNumVal) => {
          if (isProject) {
            return GraphTreeProjectImg[0]
          }
          if (isScheduleOrSubject && isNumber(statusVal)) {
            return ImageColorOptions[statusVal]
          }
          if (isStage && isNumber(stageNumVal)) {
            return GraphTreeStageImg[stageNumVal] || GraphTreeStageImg[1]
          }
          return ''
        }
        // eslint-disable-next-line prefer-const
        statusIconShape = group!.addShape('image', {
          attrs: {
            x: xStatusIcon * startX,
            y: 2.3 * startY,
            width: 16,
            height: 16,
            // eslint-disable-next-line no-nested-ternary
            img: handleGetNodeIcon(status, stageNum),
          },
          name: `image-${Math.random()}`,
        })
        // eslint-disable-next-line prefer-const
        isReply &&
          (approvalIconShape = group!.addShape('image', {
            attrs: {
              x: xApproval * startX,
              y: 0.5 * startY,
              width: 40,
              height: 35,
              // eslint-disable-next-line no-nested-ternary
              img: isReply ? ImageColorOptions[3] : '',
            },
            name: `approvalImage-${Math.random()}`,
          }))
        return Math.max(
          textShape?.getBBox().height ?? 0,
          valueShape?.getBBox().height ?? 0,
          approvalIconShape?.getBBox().height ?? 0,
          statusIconShape?.getBBox().height / 2 ?? 0
        )
      },
  • 是否居中
 fitCenter: true
  • 是否自适应
 autoFit: true
  • 是否有动画
 animate: false

数据的处理

  • 后端接口支持

  • 后端数据返回的形式,一级一级的返回,或者是数据一把搂回来

  • 当接口数据一把返回给我们,我们如何控制显示层级,坑点!!! 当如果采用的是组织架构图,那真是不好意思,人家类库还不支持控制层级,当然,你要相信Antd团队的创造力,多看文档不会让你失望。

指标拆解图的出现

Antd Charts 组织架构图 && 指标拆解图

看着样子确实差不多,但是里面可以配置的东西,可以说是完全不同!!!

通过文档案例以及API介绍

我们可以发现,指标拆解图支持异步加载控制层级显示,完美的解决我们的,一键展开、一键收起,支持单节点展开及异步加载数据功能

异步加载

Antd Charts 组织架构图 && 指标拆解图

const fetchData = () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(
          [1, 2].map(() => ({
            id: 'A2' + Math.random().toString(),
            value: {
              title: '异步节点' + Math.random().toString(),
              items: [
                {
                  text: '595万',
                },
                {
                  text: '占比',
                  value: '50%',
                  icon: 'https://gw.alipayobjects.com/zos/antfincdn/iFh9X011qd/7797962c-04b6-4d67-9143-e9d05f9778bf.png',
                },
              ],
            },
          })),
        );
      }, 1000);
    });
  };

  const getChildren = async () => {
    const asyncData = await fetchData();
    return asyncData;
  };
  
   const config = {
    data,
    autoFit: false,
    nodeCfg: {
      getChildren,
    },
    markerCfg: (cfg) => {
      return {
        show: true,
      };
    },
    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-node'],
  };

控制层级显示

Antd Charts 组织架构图 && 指标拆解图

需求解决方案:

1.后端接口一把把所有的数据返回,前端通过level控制显示层级 (完成一键展开)
2.当时,我通过条件渲染的方法,也就是写了两个图表,因为即使通过 useState() 声明一个状态,也不起作用,这个图表默认展示两层 (完成一键收起)
3.点击单节点展开,由于后端一把数据已经返回,只是通过level控制显示层级,此处无需异步加载,即可

坑点记录

  1. 数据无法set进去!!!记得检测id是否存在重复问题,当时我的后端就是,没有id给我设置为null,我又好巧不巧 id: item?.id || "1",设置节点ID默认值,排查一上午,才发现这个问题,我的内心是无比的崩溃的...
  2. 图表的tooltip,在示例上设置,能正常显示,结果本地看不到,我的内心也是十分的崩溃,一开始真的无从下手去排查,过了一会发现,它有时闪那么一下,脑子一闪,我好像设置是他的节点事件 node:mouseenter,来试验人家的方法,忘记删掉了,记得删除!记得删除!记得删除!,不然就等着崩溃,脑子宕机。。。