使用「Relation Graph」优化图表展现效果

使用「Relation Graph」优化图表展现效果

朱治龙
2022-04-11 / 0 评论 / 386 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2022年10月20日,已超过811天没有更新,若内容或图片失效,请留言反馈。

背景说明

近期检查前端工程的打包情况,发现@antv/g6占用了很大的空间:

l1daelsc.png

经过前端团队内部了解,这个主要是用于项目内超算中心的关系图展示,关系图这一块也随着业务的需求使用了不同的组件来实现:echarts > Mindelixir > @antv/G6

l1dakimf.png

实际使用@antv/G6实现完的效果,我的直观感觉也不是很理想,数据量大的情况下,显示太小,而且看了下相关的代码,感觉实现过程也不够优雅。所以秉承公司精益求精的企业文化,为了减少打包体积及优化使用体验等目的,准备使用「Relation Graph」对这一块进行大刀阔斧的改造。

经过三四天的打磨,整体效果得到了运营同事的认可,先看看实现后的效果:
relationgraph-demo.gif

本次调整后,打包体积上有较大的缩减:
优化前:(原始大小:13.69M;压缩后:4.71M;开启gzip:1.42M)
优化后:(原始大小:8.97M;压缩后:3.14M;开启gzip:957.42K)
l1dbjqj3.png

代码实现

下面将项目中关系图相关的一些核心代码进行讲解

视图代码

<relation-graph ref="relationGraph" :options="graphOptions">
        <div slot="node" slot-scope="{node}">
          <el-popover
            v-if="node.data.status === 'user'"
            trigger="hover"
            placement="right"
            popper-class="account-graph-popver"
          >
            <div slot="reference" class="node-item flex align-center">
              <div class="node-icon" :style="{color:node.data.iconColor}">
                <em class="node-icon-inner" :class="node.data.nodeIconClassName" />
              </div>
              <span class="node-name margin-left-sm" v-text="node.text" />
            </div>
            <div class="popover-tips">
              <p v-if="node.data.realname !== node.data.email && node.data.realname !== ''">
                <span>{{ $t('userCenter.name') }}</span>: {{ node.data.realname }}
              </p>
              <p><span>{{ $t('common.email') }}</span>: {{ node.data.email }}</p>
              <p v-if="node.data.phone">
                <span>{{ $t('common.mobile') }}</span>: {{ node.data.phone }}
              </p>
              <div v-if="belongingType==='use' && stopClick('accountMgmt')">
                <el-divider />
                <p class="text-center">
                  <el-button slot="reference" type="text" class="padding-lr-xs" @click="unBind(node)">
                    <para-lang code="selfService.group.accountUnbinding" />
                  </el-button>
                </p>
              </div>
            </div>
          </el-popover>
          <el-popover
            v-else-if="node.data.status !== 'accountMessage' && stopClick('accountMgmt')"
            trigger="hover"
            placement="right"
            popper-class="account-graph-popver"
          >
            <div slot="reference" class="node-item flex align-center">
              <div class="node-icon" :style="{color:node.data.iconColor}">
                <em class="node-icon-inner" :class="node.data.nodeIconClassName" />
              </div>
              <span class="node-name margin-left-sm" v-text="node.text" />
            </div>
            <div v-if="node.data.status === 'account'" class="popover-tips">
              <p><span>{{ $t('common.SCAccount') }}</span>: {{ node.text }}</p>
              <div>
                <el-divider />
                <p class="text-center">
                  <el-button v-if="belongingType==='belonging'" type="text" @click="modifyAccountRelation(node)">
                    修改所属关系
                  </el-button>
                  <el-button v-if="belongingType==='use'" type="text" @click="bindAccount(node)">
                    <para-lang code="selfService.group.accountBinding" />
                  </el-button>
                </p>
              </div>
            </div>
            <div v-if="node.data.status === 'ssc'" class="popover-tips">
              <p><span>{{ $t('common.SCCode') }}</span>: {{ node.text }}</p>
              <el-divider />
              <p class="text-center">
                <el-button type="text" @click="showAccountApply(node.text)">
                  <para-lang code="selfService.group.applicationAccount" />
                </el-button>
              </p>
            </div>
          </el-popover>
          <div v-else class="node-item flex align-center">
            <div class="node-icon" :style="{color:node.data.iconColor}">
              <em class="node-icon-inner" :class="node.data.nodeIconClassName" />
            </div>
            <span class="node-name margin-left-sm" v-text="node.text" />
          </div>
        </div>
      </relation-graph>

图标配置项

在Vue 组件的 data 里添加如下信息

graphOptions: {
        'debug': envName === 'development', // 仅开发或Istio环境下为调试模式
        'allowShowMiniView': true,
        'allowSwitchLineShape': true,
        'defaultFocusRootNode': false,
        'layouts': [
          {
            'label': '树状-横向',
            'layoutName': 'tree',
            'useLayoutStyleOptions': true,
            'layoutClassName': 'member-layout-tree-v',
            'defaultLineColor': '#DDDDDD',
            'levelDistance': '300,300,300,500',
            'min_per_width': 100,
            'max_per_width': 300,
            'min_per_height': 40,
            'max_per_height': 100,
            'defaultNodeShape': 1,
            'defaultLineShape': 6,
            'defaultJunctionPoint': 'lr',
            'defaultExpandHolderPosition': 'right',
            'from': 'left',
            'defaultNodeHeight': '36',
            'defaultNodeColor': '#C6E5FF',
            'defaultNodeBorderWidth': 1,
            'defaultNodeBorderColor': '#CED4D9',
            'defaultNodeFontColor': '#666666'
          }, {
            'label': '树状-纵向',
            'layoutName': 'tree',
            'useLayoutStyleOptions': true,
            // 'defaultFocusRootNode': false,
            'layoutClassName': 'member-layout-tree-h',
            'defaultLineColor': '#DDDDDD',
            // 'levelDistance': '120',
            'min_per_width': 100,
            'max_per_width': 400,
            'min_per_height': 100,
            'max_per_height': 500,
            'defaultNodeShape': 1,
            'defaultLineShape': 6,
            'defaultJunctionPoint': 'tb',
            'defaultExpandHolderPosition': 'bottom',
            'from': 'top',
            'defaultNodeHeight': '36',
            'defaultNodeColor': '#C6E5FF',
            'defaultNodeBorderWidth': 1,
            'defaultNodeBorderColor': '#CED4D9',
            'defaultNodeFontColor': '#666666'
          },
          {
            'label': '中心布局',
            'layoutName': 'center',
            'useLayoutStyleOptions': true,
            'layoutClassName': 'member-layout-center',
            'levelDistance': '80',
            'defaultNodeShape': 1,
            'defaultLineShape': 5,
            // 'defaultFocusRootNode': true,
            'defaultLineColor': '#DDDDDD',
            'defaultJunctionPoint': 'border',
            'defaultNodeHeight': '36',
            'defaultNodeColor': '#C6E5FF',
            'defaultNodeBorderWidth': 1,
            'defaultNodeBorderColor': '#CED4D9',
            'defaultNodeFontColor': '#666666'
          }
        ]

      }

将后端响应的数据组织成 Relation Graph 组件所需的数据

在 methods 中添加如下方法

    // 根据 RelationGraph 图表的数据组织形式,梳理内容
    buildGraphData() {
      const _this = this
      const graphData = {
        rootId: '',
        nodes: [],
        links: []
      }
      const nodes = graphData.nodes
      const links = graphData.links
      // 生成随机ID,针对数据无id属性的情况可生成一个随机ID
      const buildRandomId = function() {
        return 'mock-' + Date.now().toString(36) + '-' + Math.random().toString(36).substr(4)
      }
      var buildNodeData = function(node) {
        const nodeData = {
          'id': node.id,
          'name': node.name,
          'data': { ...node }
        }
        const nodeClassMap = {
          'ssc': { name: '超算中心', color: node.online === 1 ? '#9de173' : '#ccc', icon: 'console-svg svg-sc-center' },
          'user': { name: '用户', color: node.groupId === _this.user.userInfo.groupId ? '#C6E5FF' : '#8aabc1', iconColor: node.userId === _this.user.userInfo.group.payUserId ? '#f7c934' : '', icon: node.userId === _this.user.userInfo.group.payUserId ? 'console-svg svg-main-account' : 'console-svg svg-sub-account' },
          'account': { name: '超算账号', color: node.inSameGroup ? '#da97f9' : '#c7ceff', icon: 'console-svg svg-sc-account-2' },
          'accountMessage': { name: '账号信息', color: '#0f7dfa', icon: 'console-icon icon-sc-account' }
        }
        // nodeData.width = node.name.length * 12
        nodeData.data.nodeIconClassName = nodeClassMap[node.status].icon
        nodeData.data.iconColor = nodeClassMap[node.status].iconColor || '#666'
        nodeData.color = nodeClassMap[node.status].color || '#C6E5FF'
        if (node.status === 'accountMessage') {
          nodeData.fontColor = '#fff'
        }
        return nodeData
      }
      const buildData = function(node) {
        let parentNodeId = node.id
        if (!parentNodeId) {
          node.id = parentNodeId = buildRandomId()
        }
        if (nodes.length === 0) {
          graphData.rootId = parentNodeId
          const nodeData = buildNodeData(node)
          nodes.push(nodeData)
        }
        if (node.children && node.children.length > 0) {
          for (const childNode of node.children) {
            // 若节点没有id,则给一个随机id值
            if (!childNode.id) {
              childNode.id = buildRandomId()
            }
            const childNodeData = buildNodeData(childNode)
            const nodeLink = { from: parentNodeId, to: childNode.id, text: '' }
            nodes.push(childNodeData)
            links.push(nodeLink)
            // 递归添加子节点
            if (childNode.children && childNode.children.length > 0) {
              buildData(childNode)
            }
          }
        }
      }
      if (this.drawData && this.drawData.length > 0) {
        buildData(this.drawData[0])
      }

      return graphData
    }

获取数据并初始化组件

在 methods 中添加如下代码

initGraph() {
      this.graphInited = false
      const graphData = this.buildGraphData()
      this.$nextTick(_ => {
        this.$refs.relationGraph.setJsonData(graphData, () => {
          this.graphInited = true
        })
      })
    }

其他说明:

  • layoutName 为 force 时存在一些问题,自动时会动态调整各节点的位置,导致一些交互受到影响,所以本工程没有启用自动布局
  • 使用纵向树状布局时节点较多的情况,后面的节点容易覆盖在上一个节点上,找了下github里的issue,说可以对各节点进行绝对布局实现,但是工作量会比较大,运营没在这个点上纠结,咱也就不考虑优化了。
  • 鼠标悬停在节点上时无法触发el-popover组件的悬停事件,经排查跟节点的 + - 折叠样式有关,该节点的宽度太宽,导致底部的节点内容无法响应鼠标事件,使用如下样式覆盖默认样式完美解决(以下样式代码包含图标组件其他元素的样式调整):
.graph-container {
    ::v-deep .c-mini-graph {
      position: absolute;
      top:0;
      right:0;
      margin-top:0
    }
    ::v-deep .c-mini-toolbar {
      margin-left:0;
      margin-top:0;
      right:0;
      bottom:10px;
    }
    /* 调整样式,避免出现账号悬停不显示用户信息的情况 */
    ::v-deep .c-expand-positon-right{
      width: auto;
      right:0;
    }
    ::v-deep .c-expand-positon-bottom {
      width: auto;
      left: 50%;
      margin-left: -10px;
    }
    ::v-deep .rel-node-shape-1 {
      overflow:hidden;
      padding:0;
      border-radius: 6px;
    }
    ::v-deep .node-item {
      padding: 0 10px 0 0;
      line-height:34px;
      .node-icon {
        background-color:#fff;
        width: 36px;
        text-align: center;
        line-height: 34px;
        vertical-align: middle;
        em {
          font-size:22px;
        }
      }
      .node-name {
        white-space:nowrap;
        display: inline-block;
      }
    }
    // 布局文字定位调整
    ::v-deep .c-mb-child-panel .c-mb-button .c-mb-text{
      padding-top:3px;
      display: block;
      width: 100%;
      position: relative;
      margin: 0;
    }
  }

链接

1

评论 (0)

取消