echarts tree 视图 -- 支持缩放,支持后端接口控制节点部分样式

348 阅读6分钟

echarts 自定义的树形拓扑图,各个节点都可自定义

前言

  • 为了方便实际业务中有此类需求,先记录下此次的研究成果
  • 我这里用的 echarts 版本是 4.9.0 的,但是此案例可以支持 4.9.0 以上的均可,我会把其中关键配置截取出来,伙伴们可以直接复制粘贴到 echarts 官方网站中进行测试效果
  • (若哪里写的不对,欢迎友好评论)

效果预览

image.png

  • 后端接口可以通过如下传递如下结构的数据,控制前端视图渲染的节点和连线样式 image.png

完整代码在此!!!

<html>
<header>
  <title>zoom tree demo</title>
  <style>
    html,
    body {
      margin: 0;
      padding: 0;
    }

    .root {
      height: 100vh;
      width: 100vw;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      gap: 12px;
      background-color: rgb(176, 176, 176);
      overflow: hidden;
    }

    #zoom-tree-chart {
      background-color: black;
      border-radius: 10px;
    }
  </style>
</header>

<body>
  <div class="root">
    <div id="zoom-tree-chart" style="height:80vh;width:80vw;"></div>
    <button id="reset-btn">重制</button>
  </div>
</body>
<script src="./echarts.min.js"></script>
<script>
  // 后端接口可通过传递该结构数据,控制前端图形展示(前端会对此数据结构进行转化)
  const mockData = {
    name: '标题\n2024-01-01 11:43:55',
    color: 'green', // 节点标题文本颜色 和 节点圆点的颜色
    children: [
      {
        name: '标题12\n2024-01-01 14:18:02',
        color: 'rgba(253, 203, 110,1.0)',
        lineColor: 'red', // 节点与父节点连线颜色
        lineType: 'dotted', // 节点与父节点的连线类型; 'dotted'虚线 'solid'实线
        children: [
          {
            name: '标题1\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2'
          },
          {
            name: '标题8\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2\n2024-01-01 14:52:17',
            lineColor: 'red', // 节点连线颜色
            lineType: 'dotted', // 节点连线类型; 'dotted'虚线 'solid'实线
            children: [
              {
                name: '标题2\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2\n2024-01-01 16:06:58'
              },
              {
                name: '标题3\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2\n2024-01-01 12:52:52'
              },
              {
                name: '标题4\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2'
              },
              {
                name: '标题5\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2'
              },
              {
                name: '标题6\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2'
              }
            ],
          },
          {
            name: '标题9'
          },
          {
            name: '标题10'
          },
          {
            name: '标题11\n2024-01-01 14:52:17'
          }
        ],
      },
      {
        name: '标题13',
        color: '#E6A23C'
      }
    ]
  }

  /**
   * 将来自后端接口的数据转为 echarts 绘制图形所需的数据
   * @param {*} data 数据结构案例:{name:'标题\n其他描述\n其他描述2\n...', bgColor:'green', children:[{...}]}
   * @param {string} data.name 【必须】 规则:若需展示多内容,则需以'\n'分隔,拆分字符串后,第一个字符串作为标题,其余作为内容
   * @param {string} data.color 【可选】节点标题文本颜色 和 节点圆点的颜色
   * @param {string} data.lineColor 【可选】节点标题文本颜色 和 节点圆点的颜色
   * @param {string} data.lineType 【可选】节点与父节点的连线类型; 'dotted'虚线 'solid'实线
   * @param {string} data.children 【可选】若当前节点存在子节点,则可无限层级延伸,children 的数组元素结构同 data 本身
   * @returns 转化后满足 echarts 渲染的数据
   */
  function convertToChartData(data) {
    /* 每个节点边上的文本信息,我称之为一张卡片,下面注释中所说的卡片就是这个 */

    const cardTitleDefaultColor = '#0984e3'
    // 仅有标题时,卡片的标题样式
    const cardSingleTitleStyle = {
      backgroundColor: 'transparent',
      color: cardTitleDefaultColor,
      align: 'left',
      width: '100%',
      height: 30,
      borderRadius: 0,
      fontSize: 16
    }
    // 既有标题又有描述内容时,卡片的标题样式
    const cardTitleStyle = {
      ...cardSingleTitleStyle,
      // borderRadius: [0, 0, 0, 0]
    }
    // 卡片描述内容样式
    const cardContentStyle = {
      backgroundColor: 'transparent',
      color: '#fff',
      align: 'left',
      width: '100%',
      height: 20
      // lineHeight: 17
    }

    const nodeData = {
      // 父节点配置
      label: {
        backgroundColor: '#F4F4F4',
        borderRadius: [0, 0, 5, 5],
        rich: {},
        // formatter: [
        //   '{first|综合授信额度}', // 卡片标题
        //   '{second|(CR20190912000013)\n获批金额:100\n币种:人民币}' // 卡片内容
        // ].join('\n'),
        // rich: {
        //   // 卡片标题样式
        //   first: {
        //     backgroundColor: '#078E34',
        //     color: '#fff',
        //     align: 'center',
        //     width: 135,
        //     height: 30,
        //     borderRadius: [5, 5, 0, 0]
        //   },
        //   // 卡片内容样式
        //   second: {
        //     color: '#909399',
        //     align: 'center',
        //     lineHeight: 17
        //   }
        // }
      },
      // 节点的圆点样式
      itemStyle: {
        color: cardTitleDefaultColor, //这是节点折叠时候的颜色
        borderColor: cardTitleDefaultColor,
        borderWidth: 2,
        normal: {
          color: cardTitleDefaultColor
        },
      },
      // 节点的连线样式
      lineStyle: {
        color: '#888',
        width: 1,
        type: 'solid' //'dotted'虚线 'solid'实线
      },
    }
    // 1 -- 文本处理
    const nameString = data.name || ''
    // name 规则: /n 分隔,拆分字符串后,第一个字符串作为标题,其余作为内容
    const textAry = nameString.split('\n').filter(s => !!s)
    if (textAry.length < 1) {
      // 节点text为空时
      nodeData.label.formatter = ['{first|--}'].join('\n')
      nodeData.label.rich.first = cardSingleTitleStyle // 配置样式(对应 formatter 中的 'first' 标记)
    } else if (textAry.length === 1) {
      // 节点text只有标题时
      nodeData.label.formatter = [`{first|${textAry[0]}}`].join('\n')
      nodeData.label.rich.first = cardSingleTitleStyle // 配置样式(对应 formatter 中的 'first' 标记)
    } else {
      // textAry.length > 1
      // 节点text既有标题又其他描述时
      nodeData.label.formatter = [
        `{first|${textAry[0]}}`, // 卡片标题
        `{second|${textAry.slice(1).join('\n')}}` // 卡片内容
      ].join('\n')
      nodeData.label.rich.first = cardTitleStyle // 配置样式(对应 formatter 中的 'first' 标记)
      nodeData.label.rich.second = cardContentStyle // 配置样式(对应 formatter 中的 'second' 标记)
    }

    // ---- 根据传参数据,控制节点部分样式 ----
    // ** 允许动态配置每个节点的标题背景色 **
    if (data.color) {
      nodeData.label.rich.first.color = data.color
      nodeData.itemStyle.color = data.color
      nodeData.itemStyle.borderColor = data.color
      nodeData.itemStyle.normal.color = data.color
    }
    if (data.lineColor) {
      nodeData.lineStyle.color = data.lineColor
    }
    if (data.lineType) {
      nodeData.lineStyle.type = data.lineType
    }
    // --------

    if (Array.isArray(data.children) && data.children.length > 0) {
      nodeData.children = []
      data.children.forEach((child) => {
        nodeData.children.push(convertToChartData(child))
      })
    }
    return nodeData
  }

  /**
   * 获取树形图配置
   * @param {*} _data 参数说明请看 convertToChartData 函数描述
   * @returns
   */
  function getTreeChartOption(_data) {
    const data = convertToChartData(_data)

    const option = {
      grid: {
        left: '2%',
        right: '2%',
        bottom: '2%',
        top: '10%',
        containLabel: true
      },
      tooltip: {
        trigger: 'item',
        triggerOn: 'mousemove'
      },
      series: [
        {
          type: 'tree',
          initialTreeDepth: -1,
          data: [data],

          top: '1%',
          left: '7%',
          bottom: '1%',
          right: '20%',

          symbolSize: 10,
          zoom: 1, // 当前视角的缩放比例(初始缩放比例)
          roam: true, // 是否开启平游或缩放
          scaleLimit: {
            // 滚轮缩放的极限控制
            min: 0.5,
            max: 5
          },
          // 鼠标悬浮提示框
          tooltip: {
            show: false
          },

          // 非最后一层的节点的圆圈样式
          label: {
            normal: {
              position: 'right',
              verticalAlign: 'middle',
              align: 'left',
              color: 'black'
            }
          },

          // 控制根节点
          itemStyle: {
            color: data.itemStyle.color,
            borderColor: data.itemStyle.borderColor,
          },
          // 根节点无任何子节点时的样式(若上方 data 属性中对指定节点设置了样式,则会覆盖这里的样式)
          leaves: {
            label: {
              normal: {
                position: 'right',
                verticalAlign: 'middle',
                align: 'left'
              }
            },
            // 节点圆的默认样式(点击后会变成实心圆,且节点会收起所有子节点)(上面 data 可以针对某个节点进行设置)
            itemStyle: {
              color: data.itemStyle.color,
              borderColor: data.itemStyle.borderColor,
            },
          },
          // 节点间连线的默认样式(上面的 data 中可以针对某个节点进行设置)
          lineStyle: {
            color: data.lineStyle.color,
            width: 1,
            curveness: 0.5, // 连线的弧度(0是直线)
            type: data.lineStyle.type
          }
        }
      ]
    }
    return option
  }

  let canvasBoxDom = null
  let chart = null

  function initChart(apiData) {
    this.cleanChart()
    canvasBoxDom = document.getElementById('zoom-tree-chart')
    chart = echarts.init(canvasBoxDom)
    const option = getTreeChartOption(apiData)
    if (option && typeof option === 'object') {
      chart.setOption(option)
    }
  }
  function cleanChart() {
    if (chart) {
      chart.dispose()
    }
  }
  function reset(apiData) {
    initChart(apiData)
  }
  function resizeChart() {
    if (chart) {
      chart.resize()
    }
  }

  initChart(mockData)

  let timmer = null
  window.addEventListener('resize', ()=>{
    if(timmer != null) return
    timmer = setTimeout(()=>{
      timmer = null
      resizeChart()
    }, 100)
  })

  document.getElementById('reset-btn').addEventListener('click',() => {
    reset(mockData)
  })
</script>

</html>

我用到的 echarts 4.9.0 脚本在此!

可以直接复制到echarts官方网站测试的代码 在此!!!

image.png

// 后端接口可通过传递该结构数据,控制前端图形展示(前端会对此数据结构进行转化)
const mockData = {
  name: '标题\n2024-01-01 11:43:55',
  color: 'green', // 节点标题文本颜色 和 节点圆点的颜色
  children: [
    {
      name: '标题12\n2024-01-01 14:18:02',
      color: 'rgba(253, 203, 110,1.0)',
      lineColor: 'red', // 节点与父节点连线颜色
      lineType: 'dotted', // 节点与父节点的连线类型; 'dotted'虚线 'solid'实线
      children: [
        {
          name: '标题1\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2'
        },
        {
          name: '标题8\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2\n2024-01-01 14:52:17',
          lineColor: 'red', // 节点连线颜色
          lineType: 'dotted', // 节点连线类型; 'dotted'虚线 'solid'实线
          children: [
            {
              name: '标题2\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2\n2024-01-01 16:06:58'
            },
            {
              name: '标题3\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2\n2024-01-01 12:52:52'
            },
            {
              name: '标题4\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2'
            },
            {
              name: '标题5\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2'
            },
            {
              name: '标题6\n描述1描述1描述1描述1,描述1描述1描述1\n描述2描述2描述2'
            }
          ]
        },
        {
          name: '标题9'
        },
        {
          name: '标题10'
        },
        {
          name: '标题11\n2024-01-01 14:52:17'
        }
      ]
    },
    {
      name: '标题13',
      color: '#E6A23C'
    }
  ]
};


function convertToChartData(data) {
  

  const cardTitleDefaultColor = '#0984e3';
  // 仅有标题时,卡片的标题样式
  const cardSingleTitleStyle = {
    backgroundColor: 'rgb(16, 12, 42)', //  DarkMode
    color: cardTitleDefaultColor,
    align: 'left',
    width: '100%',
    height: 30,
    borderRadius: 0,
    fontSize: 16
  };
  // 既有标题又有描述内容时,卡片的标题样式
  const cardTitleStyle = {
    ...cardSingleTitleStyle
    // borderRadius: [0, 0, 0, 0]
  };
  // 卡片描述内容样式
  const cardContentStyle = {
    backgroundColor: 'rgb(16, 12, 42)', // DarkMode
    color: 'gray',
    align: 'left',
    width: '100%',
    height: 20
    // lineHeight: 17
  };

  const nodeData = {
    // 父节点配置
    label: {
      backgroundColor: '#F4F4F4',
      borderRadius: [0, 0, 5, 5],
      rich: {}
      // formatter: [
      //   '{first|综合授信额度}', // 卡片标题
      //   '{second|(CR20190912000013)\n获批金额:100\n币种:人民币}' // 卡片内容
      // ].join('\n'),
      // rich: {
      //   // 卡片标题样式
      //   first: {
      //     backgroundColor: '#078E34',
      //     color: '#fff',
      //     align: 'center',
      //     width: 135,
      //     height: 30,
      //     borderRadius: [5, 5, 0, 0]
      //   },
      //   // 卡片内容样式
      //   second: {
      //     color: '#909399',
      //     align: 'center',
      //     lineHeight: 17
      //   }
      // }
    },
    // 节点的圆点样式
    itemStyle: {
      color: cardTitleDefaultColor, //这是节点折叠时候的颜色
      borderColor: cardTitleDefaultColor,
      borderWidth: 2,
      normal: {
        color: cardTitleDefaultColor
      }
    },
    // 节点的连线样式
    lineStyle: {
      color: '#888',
      width: 1,
      type: 'solid' //'dotted'虚线 'solid'实线
    }
  };
  // 1 -- 文本处理
  const nameString = data.name || '';
  // name 规则: /n 分隔,拆分字符串后,第一个字符串作为标题,其余作为内容
  const textAry = nameString.split('\n').filter((s) => !!s);
  if (textAry.length < 1) {
    // 节点text为空时
    nodeData.label.formatter = ['{first|--}'].join('\n');
    nodeData.label.rich.first = cardSingleTitleStyle; // 配置样式(对应 formatter 中的 'first' 标记)
  } else if (textAry.length === 1) {
    // 节点text只有标题时
    nodeData.label.formatter = [`{first|${textAry[0]}}`].join('\n');
    nodeData.label.rich.first = cardSingleTitleStyle; // 配置样式(对应 formatter 中的 'first' 标记)
  } else {
    // textAry.length > 1
    // 节点text既有标题又其他描述时
    nodeData.label.formatter = [
      `{first|${textAry[0]}}`, // 卡片标题
      `{second|${textAry.slice(1).join('\n')}}` // 卡片内容
    ].join('\n');
    nodeData.label.rich.first = cardTitleStyle; // 配置样式(对应 formatter 中的 'first' 标记)
    nodeData.label.rich.second = cardContentStyle; // 配置样式(对应 formatter 中的 'second' 标记)
  }

  // ---- 根据传参数据,控制节点部分样式 ----
  // ** 允许动态配置每个节点的标题背景色 **
  if (data.color) {
    nodeData.label.rich.first.color = data.color;
    nodeData.itemStyle.color = data.color;
    nodeData.itemStyle.borderColor = data.color;
    nodeData.itemStyle.normal.color = data.color;
  }
  if (data.lineColor) {
    nodeData.lineStyle.color = data.lineColor;
  }
  if (data.lineType) {
    nodeData.lineStyle.type = data.lineType;
  }
  // --------

  if (Array.isArray(data.children) && data.children.length > 0) {
    nodeData.children = [];
    data.children.forEach((child) => {
      nodeData.children.push(convertToChartData(child));
    });
  }
  return nodeData;
}

const data = convertToChartData(mockData);

option = {
  tooltip: {
    trigger: 'item',
    triggerOn: 'mousemove'
  },
  series: [
    {
      type: 'tree',
      initialTreeDepth: -1,
      data: [data],

      top: '1%',
      left: '7%',
      bottom: '1%',
      right: '20%',

      symbolSize: 10,
      zoom: 1, // 当前视角的缩放比例(初始缩放比例)
      roam: true, // 是否开启平游或缩放
      scaleLimit: {
        // 滚轮缩放的极限控制
        min: 0.5,
        max: 5
      },
      // 鼠标悬浮提示框
      tooltip: {
        show: false
      },

      // 非最后一层的节点的圆圈样式
      label: {
        normal: {
          position: 'right',
          verticalAlign: 'middle',
          align: 'left',
          color: 'black'
        }
      },

      // 控制根节点
      itemStyle: {
        color: data.itemStyle.color,
        borderColor: data.itemStyle.borderColor
      },
      // 根节点无任何子节点时的样式(若上方 data 属性中对指定节点设置了样式,则会覆盖这里的样式)
      leaves: {
        label: {
          normal: {
            position: 'right',
            verticalAlign: 'middle',
            align: 'left'
          }
        },
        // 节点圆的默认样式(点击后会变成实心圆,且节点会收起所有子节点)(上面 data 可以针对某个节点进行设置)
        itemStyle: {
          color: data.itemStyle.color,
          borderColor: data.itemStyle.borderColor
        }
      },
      // 节点间连线的默认样式(上面的 data 中可以针对某个节点进行设置)
      lineStyle: {
        color: data.lineStyle.color,
        width: 1,
        curveness: 0.5, // 连线的弧度(0是直线)
        type: data.lineStyle.type
      }
    }
  ]
};