前端高手系列-echarts桑基图运用

6,125 阅读3分钟

什么是桑基图

桑基图是一种展示数据流动的利器,先来了解一下它的概念

桑基图(Sankey diagram),即桑基能量分流图,也叫桑基能量平衡图。它是一种特定类型的流程图,图中延伸的分支的宽度对应数据流量的大小,通常应用于能源、材料成分、金融等数据的可视化分析。因1898年Matthew Henry Phineas Riall Sankey绘制的“蒸汽机的能源效率图”而闻名,此后便以其名字命名为“桑基图”。

再看一下在echars官网的实例

对应的代码:

option = {
    series: {
        type: 'sankey',
        layout: 'none',
        focusNodeAdjacency: 'allEdges',
        data: [{
            name: 'a'
        }, {
            name: 'b'
        }, {
            name: 'a1'
        }, {
            name: 'a2'
        }, {
            name: 'b1'
        }, {
            name: 'c'
        }],
        links: [{
            source: 'a',
            target: 'a1',
            value: 5
        }, {
            source: 'a',
            target: 'a2',
            value: 3
        }, {
            source: 'b',
            target: 'b1',
            value: 8
        }, {
            source: 'a',
            target: 'b1',
            value: 3
        }, {
            source: 'b1',
            target: 'a1',
            value: 1
        }, {
            source: 'b1',
            target: 'c',
            value: 2
        }]
    }
};

从代码可以看出,echars的桑基图主要有两个基本概念:

  • 节点 (nodes):表示所有有关系的节点
  • 连接 (links): 源节点至目标节点之间的关系,每个连接包括三个元素:
    • source: 源节点
    • target: 目标节点
    • value: 数据

桑基图的作用:

可用于数据从一系列节点到另一系列节点流入流出的可视化。例如: 图书章节,资金流向,渠道分析,用户行为分析。

开始实践

需求场景

做一个app用户行为分析,点击某个节点或支流流向,能后点亮整个链路。

实践效果

代码实现

初步配置达到预览效果,可以将以下代码复制到echarts的编辑器中查看效果

option = {
    series: {
        type: 'sankey',
        layout: 'none',
        focusNodeAdjacency: 'allEdges',
        data: [{
            name: '进入',
         
           
        }, {
            name: '首页'
            
        }, {
            name: '个人中心'
        }, {
            name: '购物车'
        },
         {
            name: '商品'
        },
          {
            name: '订单'
        }],
        links: [{
     
      source: '进入',
      target: '首页',
      value: 5,
    },
    {
      source: '进入',
      target: '个人中心',
  
      value: 3,
    },
    {
   
      source: '个人中心',
      target: '购物车',
      value: 8,
    },
    {
    
      source: '购物车',
      target: '商品',
      value: 8,
    },
    {
    
      source: '个人中心',
      target: '订单',
      value: 9,
    },]
    }
};


实现能点击某个节点或支流流向,能后点亮整个链路。

myChart.showLoading()

let option
let links=[]
$.get(ROOT_PATH + '/data/asset/data/energy.json', function (data) {
  myChart.hideLoading()
  let node = [
    {
      name: '进入',
    },
    {
      name: '首页',
    },
    {
      name: '个人中心',
    },
    {
      name: '购物车',
    },
    {
      name: '商品',
    },
    {
      name: '订单',
    },
  ]
  links = [
    {
     
      source: '进入',
      target: '首页',
      value: 5,
    },
    {
      source: '进入',
      target: '个人中心',
  
      value: 3,
    },
    {
   
      source: '个人中心',
      target: '购物车',
      value: 8,
    },
    {
    
      source: '购物车',
      target: '商品',
      value: 8,
    },
    {
    
      source: '个人中心',
      target: '订单',
      value: 9,
    },
  ]
  option = {
    title: {
      text: 'Sankey Diagram',
    },
    tooltip: {
      trigger: 'item',
      triggerOn: 'mousemove',
    },
    series: [
      {
        type: 'sankey',
        data: node,
        links: links,
        // focusNodeAdjacency: 'allEdges',
        itemStyle: {
          borderWidth: 1,
          borderColor: '#aaa',
        },
        lineStyle: {
          color: 'source',
          curveness: 0.5,
        },
      },
    ],
  }
  getLastLevelNodes()
  findFirstNode()
  myChart.setOption(option)
})
let currentItemObj
let rootLinks = []
let leftNodesArr = []
let rightNodesArr = []
let lastLevelNodesArr = []
let headsArr = [];
function findFirstNode(){
	//当前source 不在其他target中
	  	let headNodesArr = [];
		  links.forEach((link,index,arr) => {
			  if(arr.every((i)=>{
				 return link.source !=i.target
			  })){
				 headNodesArr.push(link.source)
				}	  
				  
  })
	headsArr = Array.from(new Set(headNodesArr))
	console.log('headsArr',headsArr)
}

function isLastLevelNode(node) {
  let sourceArr = []
  let targetArr = []
  option.series[0].links.forEach((link) => {
    if (node.name === link.source) {
      sourceArr.push(link)
    } else if (node.name === link.target) {
      targetArr.push(link)
    }
  })
  return targetArr.length > 0 && sourceArr.length === 0
}
// 拿到所有末级节点数组
function getLastLevelNodes() {
  lastLevelNodesArr = []
  option.series[0].data.forEach((node) => {
    if (isLastLevelNode(node)) {
      lastLevelNodesArr.push(node)
    }
  })
}
function findLeftLink(obj) {
  console.log('findLeftLink obj', obj)
  if (!Object.prototype.toString.call(obj) === '[object Object]') return
  leftNodesArr.push(obj)

  if(headsArr.includes(obj.source)){
          return ''
  }
  let nextObj = option.series[0].links.find((item) => item.target == obj.source)
  console.log('findLeftLink nextObj', nextObj)

  return nextObj && findLeftLink(nextObj)
}

function findRightLink(obj) {

  if (!obj) {
    return
  }
  rightNodesArr.push(obj)
  option.series[0].links
    .filter((item) => item.source == obj.target)
    .map((i) => findRightLink(i))
}

myChart.on('click', function (params) {
  console.log('params:', params)
  console.log('links:', option.series[0].links)
  option.series[0].links.forEach((item) => {
    if (item.hasOwnProperty('value')) {
      delete item.lineStyle
    }
  })
  let currentItem = params.data
  leftNodesArr = []
  rightNodesArr = []
  rootLinks = []
  if (!currentItem.hasOwnProperty('value')) {
    // 点击的节点
    console.log('currentItem:点击的节点', currentItem)
    currentItemObj = option.series[0].links.find(
      (o) => o.target == currentItem.name
    )
    if (currentItemObj) {
      findLeftLink(currentItemObj)
      // 如果不是是末级节点
      if (!lastLevelNodesArr.find((v) => v.name === currentItem.name)) {
        findRightLink(currentItemObj)
      }
    } else {
      // 如果是1级节点
    
      option.series[0].links.forEach((link) => {
        if (link.source == currentItem.name) {
          console.log('nn link:', link)
          findRightLink(link)
        }
      })
    }
    console.log('currentItem:点击的节点', currentItemObj)
  } else {
    // 点击的线
    console.log('currentItem: 点击的线', currentItem)
    currentItemObj = currentItem
    findLeftLink(currentItemObj)
    findRightLink(currentItemObj)
  }

  console.log('左边的节点leftNodesArr', leftNodesArr)
  console.log('右边的节点rightNodesArr', rightNodesArr)

  // let matchedNodes = Array.from(new Set(leftNodesArr.concat(rightNodesArr)))
  // console.log('matchedNodes', matchedNodes)
  if (!currentItem.hasOwnProperty('value')) {
      //第一个是不是根节点
    if (rightNodesArr.length > 1 && !headsArr.includes(rightNodesArr[1].source)) {
      
      rightNodesArr = rightNodesArr.slice(1)
    }
  }
  console.log('右边的节点rightNodesArr', rightNodesArr)
  option.series[0].links.forEach((item) => {
    rightNodesArr.forEach((v) => {
      if (item.target === v.target) {
        item.lineStyle = {
          color: 'red',
        }
      }
    })
  })

  myChart.setOption(option)
})

运用的知识点

  • 函数递归
  • 数组去重
  • echats操作