Vue+Echarts自定义可视化图表组件

4,519 阅读2分钟

由于最近的项目中使用echarts的部分比较多,因此把echarts的方法封装成了一个组件,方便调用。这算是我自己的项目总结吧,也希望能给刚接触echarts的小伙伴们一点帮助~

因为是在一个组件中处理多个图表,所以需要根据图表的类型进行判断。放在一个组件里处理的优点是方便调用,比较适合绘制风格差不多的简单图表;缺点是每个图表的自定义部分不多,如果需要增加一些特殊的功能,还需要逐个补充。

安装

项目搭建完成之后可以使用npm安装echarts:

npm i Echarts -S

组件

新建文件chart.vue并引入echarts:

chart.vue

props

由于这个组件要实现多种图表,因此需要从外部传入图表数据和图表类型,方便根据绘图类型生成相应的配置项。

所以需要从父组件传递进来的参数有:

props:{
   domId: String,//图表容器的id
   type: {type: String, default: 'bar'},//图的类型
   chartData: Array,//数据
   custom: {//自定义配置项
       type: Object,
       default: function () {
        return {}
       }
   },
}

其中,绘图的容器id必须是唯一的。custom是自定义的样式,一般可以不传。以下是我定义的简单的custom:

{
    title: String,//标题
    xAxisname: String,//category轴的名字
    yAxisname: String,//value轴的名字
    color: Array,//颜色值数组
    showLegend: Boolean,//是否显示图例
    showRing: Boolean,//饼图显示成环形图
    vertical: Boolean,//柱状图是否横向
    rotate: Number, //坐标刻度标签的旋转角度
    gridShow: Boolean,//是否显示网格区域
    barMaxWidth: Number,//柱子的最大宽度
    lineSymbol: String,//折线拐点图形
    stack: Object,//堆叠值,例如{comment: 'stackname',share: 'stackname'}
    clickParam: Function,//点击事件
    clickGrid: Function,//网格点击事件
    dataTypes: Object, //例如{like: '点赞',comment: '评论',share: '转发'}
    types: Object, //多个图形对应的字段,例如{like: 'line',comment: 'bar',share: 'bar'}
}

调用画图的方法

mounted(){
    // 初始化
    let chartDom = document.getElementById(this.domId)
    this.chart = echarts.init(chartDom)
    // 调用绘图方法
    this.getChartsById()
}

监听数据变化

如果数据变化了就重新绘图:

watch:{
    chartData:{
        handler: function (n) {
            this.$nextTick(()=>{
                this.getChartsById()
            })
        },
        deep:true
    }
}

监听窗口变化

如果容器的大小是随窗口变化,那么在容器变化时,也会重新调整图表的大小:

window.onresize = () => {
    if(this.chart) this.chart.resize()
}

如果绘制多个图,那么在chart中resize只会改变最后一个绘图的大小。解决办法:在父组件中用数组push所有图的实例,然后循环resize。

在父组件中调用

组件的基础用法只用传id和数据:

<chart :chart-data="data" dom-id="chart1"></chart>

示例:

parent.vue
效果:
test

画图

定义要调用的方法:

getChartsById(){}

我们需要在getChartsById这个方法中,定义一些公用的变量,然后设置图表实例的配置项以及数据。 因此,需要做以下几件事:

  1. 初始化(这一步可以放在mounted中)
  2. 清空原来的图
this.chart.clear()
  1. 判断数据是否为空
let data = this.chartData
if(!data || data.length === 0) return
  1. 定义全局变量
//颜色
let color = this.custom.color||['#3aa1ff','#5cbd91','#ffbf63','#5683f5']
let options = {
    color : color,
    tooltip : {//提示框
        trigger : this.type == 'pie'? 'item':'axis',
        axisPointer : {
        	type : 'shadow',
        },
    }
}
//是否显示图例
if(this.custom.showLegend){
	options.legend = this.getLegend()
}
//是否显示标题
if(this.custom.title){
    options.title = {
        text: this.custom.title
    }
}
  1. 判断绘图类型
switch(this.type){
    case 'bar':
    case 'line':
        //坐标系网格区域
		options.grid = this.getGrid()
        //横坐标
        let axis = this.getxAxis()
        if(this.custom.vertical){//纵向图
            options.xAxis = axis.valueAxis
            options.yAxis = axis.categoryAxis
        }else{//横向图
            options.xAxis = axis.categoryAxis
            options.yAxis = axis.valueAxis
        }
        options.series = this.getBarOrLineSeries()
        break;
    case 'pie':
        options.series = this.getPieSeries(color)
        break;
    case 'wordCloud':
        options.series = this.getCloudSeries()
        break;
    default:
        break;
}
if(this.type === 'bar'){
    options.dataZoom = this.getDataZoom(color)
}
  1. 插入事件

点击图形事件:

if(this.custom.clickParam){
    this.chart.on('click',param => {
    	this.custom.clickParam(param)
    })
}

有时图形太小了难以点击,此时可以选择点击网格区域,获取点击位置:

if(this.custom.clickGrid){
    this.chart.getZr().on('click',params => {
        const pointInPixel = [params.offsetX, params.offsetY];
        if (this.chart.containPixel('grid',pointInPixel)) {
            let offset = this.chart.convertFromPixel({seriesIndex:0},[params.offsetX, params.offsetY]);
            let xIndex = this.custom.vertical?offset[1]:offset[0];
            /*事件处理代码书写位置*/
            let item = data[xIndex]
            this.custom.clickGrid(item);
        }
    });
}
  1. 绘图
this.chart.setOption(options)

需要坐标轴的图

坐标轴

折线图和柱状图需要获取坐标轴:

getxAxis(){
    //坐标轴数据
    let xAxisData = this.chartData.map(val=>val.name)
    //坐标轴样式
    let axisStyle = {
        axisLine : {
            lineStyle : {
            	color : '#cfcfcf'
            }
        },
    }
    let categoryAxis = Object.assign({}, axisStyle,{
        type : "category",
        data : xAxisData,
        axisLabel : {
            rotate : this.custom.rotate||0
        },
        name: this.custom.xAxisName||''
    })
    let valueAxis = Object.assign({}, axisStyle,{
        type : "value",
        name: this.custom.yAxisName||''
    })
    return {categoryAxis,valueAxis}
}

网格区域

getGrid(){
    //坐标系网格区域
    return {
        show: this.custom.gridShow || false,
        left : this.custom.gridLeft || 20,
        right : this.custom.gridRight || 20,
        top : this.custom.gridTop || 20,
        bottom : this.custom.gridBottom || 20,
        containLabel : true,
    }
}

柱状图和折线图

柱状图和折线的区别其实不大,所以我们统一处理,先定义初始的样式:

getBarOrLineItem(type=this.type,title,stack){
    //单个样式
    let chartItem = {
        name : title || this.custom.name,
        type : type,
    }
    if(this.type === 'bar'){
        chartItem.barMinHeight = 1
        chartItem.barMaxWidth = this.custom.barMaxWidth||20 //柱子最大宽度
    }else{
        chartItem.symbolSize = 9
        chartItem.symbol = this.custom.lineSymbol||'emptyCircle'
    }
    //堆叠图
    stack && (chartItem.stack = stack)
    return chartItem
}

有时候需要绘制多个柱子或者多个折线,绘制单个图形和组合图的数据会不一样。我定义的数据格式示例如下:

//单个数据
data = [{name:'张三',value:10},{name:'李四',value:20},,{name:'王五',value:30}]
//多个数据
datas = [{name:'张三',tweets:10,fans:1},{name:'李四',tweets:20,fans:10},{name:'王五',tweets:30,fans:5}]

一般我会在custom中传递一个dataTypes,以便区分不同的数据所对应的字段。例如:

dataTypes: {
    tweets:'推文',
    fans:'粉丝'
}

继续承接上面获取柱状图和折线图的方法。首先需要判断是要绘制多个图形还是单个图形,单个图形就直接赋值,多个图形的情况稍微复杂一些:

getBarOrLineSeries(type = this.type){
    let series = []
    if(this.custom.dataTypes){
        // 多个图形
        let dataObj = {}
        let dataTypes = this.custom.dataTypes
        for(let dataType in dataTypes){
            //数据
            dataObj[dataType] = this.chartData.map(item => {
                return {name: item.name, value: item[dataType]||0}
            })
            //堆叠
            let stack = this.custom.stack && this.custom.stack[dataType]
            //多种类型。如果不传types,默认类型都是this.type
            if(this.custom.types){
            	type = this.custom.types[dataType]
            }
            let newItem = this.getBarOrLineItem(type,dataType,stack)
            newItem.data = dataObj[dataType]
            series.push(newItem)
        }
    }else{
        // 单个图
        let chartItem = this.getBarOrLineItem()
        chartItem.data = this.chartData
        series.push(chartItem)
    }
    return series
}

不需要坐标轴的图

饼图

饼图的处理很简单:

getPieSeries(color){
    //初始的饼图
    let pieSeries = {
        name : this.custom.name || '',
        type : 'pie',
        center : [ '50%', '50%' ], // 位置
        radius : [ 0, '68%' ], // 半径
        selectedMode : true,
        hoverAnimation : true,
        data : this.chartData,
        color : color,
        animationType : 'scale',
        animationEasing : 'elasticOut',
        animationDelay : function(idx) {
            return Math.random() * 100;
        }
    }
    //显示圆环图
    this.custom.showRing && (pieSeries.radius = [ '37%', '68%' ])
    return pieSeries
}

词云

由于echarts主要部分已经去掉了word-cloud词云图,如果要绘制词云图需要再安装一个插件:

npm i echarts-wordcloud -S

然后在echarts后面引入这个插件:

import wordcloud from 'echarts-wordcloud'

之后就可以照常使用了。

getCloudSeries(){
    let wordSeries = {
        type: 'wordCloud',
        //文字大小
        sizeRange: [11, 40],
        rotationRange: [-45, 45],
        autoSize: {
            enable: true,
            minSize: 6
        },
        //文字样式
        textStyle: {
            normal: {
                color: function(param) {
                    return 'rgb(' + [
                        Math.round(Math.random() * 160),
                        Math.round(Math.random() * 160),
                        Math.round(Math.random() * 160)
                    ].join(',') + ')';
                },
            },
            emphasis: {
                shadowBlur: 10,
                shadowColor: '#666'
            }
        },
        data: this.chartData
    }
    return wordSeries
}

其他

图例

有时候组合图和饼图需要显示图例:

getLegend(){
    let legendData = []
    //饼图
    if(this.type === 'pie'){
        legendData = this.chartData.map(val=>val.name)
    }
    //获取组合图的类型
    else if(this.custom.dataTypes){
        for(let t in this.custom.dataTypes){
            legendData.push(this.custom.dataTypes[t])
        }
    }
    let legend = {
        data: legendData,
        itemHeight: 10,
        itemWidth: 12,
        orient: 'horizontal',
        right: 0,
        textStyle: {
            color:'#666'
        },
    }
    return legend
}

滚动条

在柱状图数据很多的时候,可以显示滚动条:

getDataZoom(color){
    let length = this.chartData.length
    let dataZoom = {
        type : 'slider',
        show: length > 10,
        bottom : 6,
        startValue : 0,
        endValue: 9,
        height:10,
        fillerColor: color[0],
        borderColor: color[0],
        handleIcon : 'M-292,322.2c-3.2,0-6.4-0.6-9.3-1.9c-2.9-1.2-5.4-2.9-7.6-5.1s-3.9-4.8-5.1-7.6c-1.3-3-1.9-6.1-1.9-9.3c0-3.2,0.6-6.4,1.9-9.3c1.2-2.9,2.9-5.4,5.1-7.6s4.8-3.9,7.6-5.1c3-1.3,6.1-1.9,9.3-1.9c3.2,0,6.4,0.6,9.3,1.9c2.9,1.2,5.4,2.9,7.6,5.1s3.9,4.8,5.1,7.6c1.3,3,1.9,6.1,1.9,9.3c0,3.2-0.6,6.4-1.9,9.3c-1.2,2.9-2.9,5.4-5.1,7.6s-4.8,3.9-7.6,5.1C-285.6,321.5-288.8,322.2-292,322.2z',
        handleSize : 10,
    }
    // 横向图
    if(this.custom.vertical){
        let dom = document.getElementById(this.domId)
        dataZoom.yAxisIndex = 0
        dataZoom.height = dom.offsetHeight? dom.offsetHeight- 40 : 150
        dataZoom.width = 10
    }
    return dataZoom
}

高亮事件

外部触发选中图形高亮:

highlight
子组件chart.vue中:

highLightBar(list){
    let newSeries = []
    for(let i = 0; i < this.chart.getOption().series.length; i++){
        newSeries.push({
            itemStyle: {
                normal: {
                    color: function (param){
                        return list.indexOf(param.name) === -1 ? param.color : '#fa423c';
                    }
                },
            }
        })
    }
    this.chart.setOption({
        series: newSeries
    })
}

父组件中:

selectBar(){
    this.$refs.chart1.highLightBar(this.selectNames)
},

最后

以上都是简单的图表样式,更炫酷的效果还得小伙伴们自己补充哈~

因为是初次在vue项目中使用echarts,所以还有一些不足,如果有改进意见麻烦给我留言鸭(´・ω・`)ノ