(2)基于Echarts插件的多维数据可视化设计和实现

前言:本文针对多维离散数据和多维连续数据,分别构建多维可视化图表。由于离散和连续图表展示的效果差别大,故多维图表的可视化,分为两大部分进行设计和实现。

总体设计思想:主要是采用类似树形图结合柱状图、线图、散点图、面积图。树形图主要展示的是维度的值,和各个维度值之间的关系。如图(1)所示。第一条数据:[“企业”, “个人政府征订”,”奉化站”,”2013-07”, 6500],在图表中与这条数据相关的内容,除了第一根蓝色方形,还包括红线相连的名称(企业,个人政府征订,奉化站)。如果把图中所有的名称相互连接起来,像图中红线一样,是不是感觉像一棵树?特别要注意的是(企业-个人政府征订-奉化站) 可以看成一个分组,在这个分组里可能有不止一个值,它分布着多条数据。

这里写图片描述

图1:多维数据展示

总体实现方式:要实现如图1所示的效果,可以采用Echarts插件的各种组件经过一系列的计算和整合,当然其中肯定涉及插件若干接口的改造和新增。最终生成的图表才能展示出我们需要的效果。

多维离散型数据的设计思路

多维离散型数据的定义:所有的维度都是离散的,不存在任何连续型维度。对于多维离散型数据,由于在每一条数据相互之间在轴上并没直接关系,关系同属于一个分组。对于同一个度量,各个分组展示出来的图表类型应该是一致的。(特殊情况,对于三个维度的数据,类目气泡图也是可以展示的)

基于Echarts插件的多维离散型数据可视化实现
一、问题阐释
1. 基于Echarts插件,要实现多维数据可视化,是否有官方接口,如果有官方接口不就轻松搞定?
通过查看Echarts的文档,发现确实有一个图表类型,可以展示多维数据,如图2所示,但是这种展示方式,貌似并不能适合所有多维数据,而且可视化的效果有点差强人意。
这里写图片描述

图2 Echarts的多维数据展示

2. 如果要实现我们设计的树形加普通图表的展示方式,如何实现树形?如何进行分组?每一个分组高和宽占用整个图表高和宽多少?每一个分组由于是同一个度量,是不是应该共享同一个度量轴?
对于这些问题,暂时不做回答。接下来,我会手把手教大家如何解决这些问题,最终实现整个多维图表。

二 多维数据的结构
要实现整个设计,一共需要三个数据结果,我把它们取名:data,measure, dimension。具体的数据例子,如下所示,data:是一个二级数组[[d1,..,dn,m1,..,mn],…,[d1,..,dn,m1,..,mn]] (dn:表示第n个维度的信息,而mn表示n个度量的信息),它存储着图表要显示的多维数据详情,每一条数据时一个数组,数组的前几位是维度的值,后几位是度量的值。measure:是一个数组,它存储着度量字段的相关信息,数组内每条数据时一个js对象,对象的key属性,是度量的关键字,用于区分其他度量;alias属性时度量的别名,用于显示度量的中文名称;state属性表示度量所对应的图表类型。dimension :也是一个数组,构造跟measure差不多,公共的属性表示的含义和measure是一样的,continuity字段表示的是维度的连续性,’11’表示是连续的。colType:表示维度的数据类型。如果是离散的,维度的数据类型一般都是字符串型的。

var data = [
    [
        "企业",
        "余姚站",
        "2013-05",
        0.015
    ],  
    ...
    [
        "普通客户",
        "仓基站",
        "2013-01",
        0.019
    ]
];
var dimension = [
    {
        "key":"cust_type",       
        "alias":"cust_type",
        "continuity":"00",
        "colType":"string"
    },
    ....
    {
        "key":"Month",     
        "alias":"Month", 
        "continuity":"11",
        "colType":"date"
    }
];
var measure =      
[
    {
        "key":"huan_bi",     
        "alias":"总和_huan_bi",            
        "state":"bar"
    },
    ...
     {
        "key":"tong_bi",
        "alias":"总和_tong_bi",                  
        "state":"bar"
    }
];

三、Echarts接口的扩展和新增

为了解决(1)提出的问题,在Echarts的接口一共需要扩展一个接口,新增2个接口。
扩展的接口主要是
Axis.prototype.dataToCoord;该接口主要负责计算名称或者叫标签的坐标值;在Echarts中标签值叫label,而字符串的label一般用category离散的轴表示,每一个label之间的距离是等距的。这个与我们要求是有出入的,因为在多维图表中,每一个label之间并不一定是等距的,所以我们要定距给其一个值,使之满足我们的要求。而这个值的计算是在Echarts插件外部进行的,由函数calLabelProperty完成。

calLabelProperty函数的功能,主要是计算跟label定位和显示相关的三个属性值,line:表示两个label之间的分割线;area表示每条label信息在图表中所占的宽度;curBit表示每条label在图表上的位置。代码中normalizeData变量保存着每一个label在一条轴上的比率值,介于0和1之间。

 dataToCoord: function (data, clamp) {
                var extent = this._extent;
                var scale = this.scale;
                var normalizeData = this.model.get('normalizeData');
                //沈才良 @face
                if (normalizeData) {
                    data = normalizeData[data];
                } else {
                    data = scale.normalize(data);
                }

                if (this.onBand && scale.type === 'ordinal') {
                    extent = extent.slice();
                    fixExtentWithBands(extent, scale.count());
                }

                return linearMap(data, normalizedExtent, extent, clamp);
            },
calLabelProperty: function(chartLabelData, isContinue) {
    var categoryLabel = [];
    var sum = 0, labelArray, percentArray, areaArray;
    var curSum = 0, curBit = 0;
    var curBitArray = [];
    var classNum = chartLabelData.length;

    for (var i = 0; i < classNum; i++) {                
        //如果图表是离散的,则是label的总和
        sum = isContinue ? chartLabelData[classNum - 1].labelData.length : chartLabelData[i].sum;
        //当前的统计和
        curSum = 0;
        categoryLabel[i] = {};
        /*
           labelArray: 保存每一个维度上的label
           lineArray: 保存各个label之间的分隔线的位置
           curBitArray: 保存各个label的位置
           areaArray: 保存各个label在图表上所占的区域宽
        */
        labelArray = categoryLabel[i].label = [];
        lineArray = categoryLabel[i].line = [];
        curBitArray = categoryLabel[i].percent = [];
        areaArray = categoryLabel[i].area = [];
        //求出每一个类别子项的显示位置的值
        for(var j = 0, dataItem; j < chartLabelData[i].labelData.length; j++) {
            dataItem = chartLabelData[i].labelData[j];  
            //连续的时候,离散的最后一个维度计算curSum等于 j + 1               
            if (isContinue && i == (classNum - 1)) {
                curSum = j + 1;
            } else {
                curSum += dataItem.count;
            }   
             //删除label里面的换行符号
            labelArray.push((dataItem.labelName + '').replace(/\r\n/g, ' '));
            lineArray.push(curSum / sum);
        }
        //每一个维度的第一个分组信息计算,跟其他分组不一样
        curBitArray[0] = lineArray[0] / 2;
        areaArray[0] = lineArray[0];
        for ( j = 1; j < lineArray.length; j++) {
            areaArray.push(lineArray[j] - lineArray[j-1]);
            curBitArray.push(lineArray[j - 1] + (lineArray[j] - lineArray[j - 1]) / 2);
        }
    }
    return categoryLabel;
},

新增的接口主要是:label之间的分割线、悬浮label所占区域时候区域背景色改变的变化接口。
新增接口的定义在AxisBuilder构造函数所属区域中builders.axisLabel函数中。

   //添加标签的hover事件
    buildHoverRect({
        group: this.group, 
        pos: pos, 
        textEl: textEl, 
        axisModel: axisModel, 
        contentBit: areaData[index]
    });
    /**
      * 当用户悬浮在label上面
      * 则显示整个label分组所占的空间
      * 由于zrender默认的hide和show函数
      * 当数据超过100多时,性能差,故采用
      * 设置opacity达到显示和隐藏的效果
    */
    textEl.on('mouseover', function (event) {
        this.hoverRect.setStyle('opacity', 0.24);
    });
    /**
       * opacity = 0表示隐藏该label有颜色背景
       * textEl:表示每一个label在图表上的实体
    */
    textEl.on('mouseout', function (event) {
        this.hoverRect.setStyle('opacity', 0);
    });
    /**
        * 生成hover时候出现的矩形和背景
        * @param {Object} params: userful params to build hover rect
        * @param {Number} contentBit: 每一个label所在的区域长度,[0,1]之间
        * @param {Array.<Number>} pos: label的坐标值,二维的
    */
    function buildHoverRect(params) {   
        var axisModel = params.axisModel;
        var coordinateRect = axisModel.coordinateSystem && axisModel.coordinateSystem._rect;
        var extent = axisModel.axis.getExtent();     
        var axisWidth = extent[1] - extent[0];  
        //得到显示区域的宽度大小       
        var contentWidth = Math.ceil(axisWidth * params.contentBit);
        var rect = new graphic.Rect({
            shape: {
                x: params.pos[0] + coordinateRect.x - contentWidth / 2,
                y: params.pos[1] + coordinateRect.y - 7, 
                height: 28,
                width: contentWidth
            },
            z: 2,
            style: {
                 fill:'green',
                 opacity: 0,
                 lineWidth: 1
            },
            silent: false
        });
        //添加到group中,以便echarts统一绘制
        params.group.add(rect);
        //方便隐藏和显示
        params.textEl.hoverRect = rect;
    }

第二个重要的接口新增是buildSplitLine函数,主要负责构建label之间的分隔线。具体代码和注释如下所示:

/**
  * 生成label之间的分割线,方便用户识别不同的label分组
   * {Array} group:分组组件,生成的line只有放入这个组件中,才能统一绘制
   * {Object} axisModel: 坐标轴的model模型,主要保存与坐标轴相关的信息
*/
function buildSplitLine(group, axisModel) {
    var axisType = axisModel.mainType;
    var show = axisModel.get('classSplitLine.show');
    if (axisType != "singleAxis" || show !== true) {
        return;
    }
    //获取分割线的所需的数据,由外部接口calLabelProperty计算出
    var data = axisModel.get('classSplitLine.data');
    var coordinateRect = axisModel.coordinateSystem && axisModel.coordinateSystem._rect;
    var extent = axisModel.axis.getExtent();     
    var axisWidth = extent[1] - extent[0];  
    var point1 = [];
    var point2 = [];
    for (var i = 0 ; i < data.length - 1; i++) {
        //求出分隔线两个端点的坐标值
        point1[0] = data[i] * axisWidth + coordinateRect.x;
        point1[1] = coordinateRect.y;
        point2[0] = data[i] * axisWidth + coordinateRect.x;
        point2[1] = coordinateRect.y + 26;  
        var line = new graphic.Line(graphic.subPixelOptimizeLine({
            shape: {
                x1: point1[0],
                y1: point1[1],
                x2: point2[0],
                y2: point2[1]
            },
            style: {
                 stroke: "#d3d3d3",
                 color: '#666',
                // lineDash: [4, 4],
                 opacity: 0.5,
                 lineWidth: 1
            },
            z2: 3,
            silent: true
        }));
        group.add(line);
    }
}

这几个接口具体定义的位置,请参考我详细的源码。
源码下载地址:http://download.csdn.net/download/mulumeng981/9985030 (基于Echarts最新版3.7.1)

如果你注意到一个不准确或似乎不太正确的地方,请让我知道。谢谢!

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值