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

说完了内部扩展和新增接口,接着就是外部的接口了。
外部的接口主要对原始数据进行一系列的计算、变换、包装,然后放入option各个参数中。
createChart接口

/**
   * 通过Echarts的setOption接口建立echarts多维图表
*/
createChart: function () {
    //optionData由外部接口计算出的配置项数据
    var optionData = this.optionData;
    var option = {
        color:this.colors,
        grid:optionData.gridData,
        tooltip: {
            trigger: 'item'
        },
        //图例默认为垂直放置
        legend: {
            show: true,
            orient: 'vertical',
            data: optionData.legendData,
            right: 0,
            top: 60,
            formatter: function (name) {
                return echarts.format.truncateText(name, 80, '14px Microsoft Yahei', '…');
            },
            tooltip:{
                show: true
            }
        },
        calculable: true,
        singleAxis: optionData.singleAxisData,
        xAxis:optionData.xAxisData,
        yAxis: optionData.yAxisData,
        series: optionData.seriesData
    };
    if(optionData.formatter){
        option.tooltip.formatter = optionData.formatter;
    }               
    this.chart.setOption(option);           
},

从上面代码中可以看到,多维图表的构造和普通图表并没有本质上的区别,只是传入的参数不同而已。
不知大家有木有发现,在上面代码中optionData是一个关键变量,各种赋值都跟它密切相关。

getChartOption接口: 而 optionData是从何处来的呢?在我的设计中getChartOption函数经过一系列的处理和计算,最终得到optionData所需的数据。getChartOption函数代码较多,其实还可以继续划分为几个小函数,这样代码看起来更优雅一些。

/**
 * 获取图表的option配置项 
*/
getChartOption: function () {
    var that = this;
    var state = that.properties.state;
    var measure = that.properties.measure;
    var dimension = that.properties.dimension;
    var dLen = dimension.length;
    var mLen = measure.length;
    var visualMLen = mLen === 0 ? 1 : mLen;
    var chart = this.chart;
    var color = this.colors;
    var finalData = that.finalData;
    // 求出图例的数据
    var legendData = mLen > 0 && measure.map(function(item) {
            return item.alias;
        });
    // 获取维度中连续的字段
    var continuousKey = that.getContinueKey(dimension, mLen);
    // 该展示连续和离散图表
    var isContinue = continuousKey.length > 0;
    // 将原始数据进行标准化,得到按维度分类的数据
    var categoryData = that.standarHigherData(finalData, measure, dimension);
    // 计算出各级维度label显示需要的各种属性信息,如:label坐标值、所占区域宽度、分隔线坐标值
    var percentData = that.calLabelProperty(categoryData.chartLabelData, continuousKey.length > 0);
    var chartData = categoryData.chartData;
    var peakValue = categoryData.peakValue;
    //图表到左边的距离
    var LEFT_DISTANCE = mLen === 0 ? 10 : 60;
    var sum = categoryData.chartLabelData[0] && categoryData.chartLabelData[0].sum;
    var showClassNum = categoryData.chartLabelData.length;
    //图表grid到图表最上面的距离
    var TOP_DISTANCE = 28;
    //图表grid的x轴label所占的高度
    var AXIS_LABEL_HEIGHT = 50;
    //图表图形显示区域到图表最右边的距离
    var RIGHT_DISTANCE = 100;
    //图表图形实际显示宽度
    var realChartWidth = this.getWidth() - LEFT_DISTANCE - RIGHT_DISTANCE;
    //图表图形实际显示高度
    var realChartHeight = this.getHeight() - (showClassNum == 0 ? 14 : TOP_DISTANCE * showClassNum) - (visualMLen - 1 ) * 10 - AXIS_LABEL_HEIGHT;
    //图表每一个grid所占的宽度
    var perChartWidth = realChartWidth / (isContinue ? chartData.length : sum);
    var xAxisData = [], yAxisData = [], gridData = [], seriesData = [], singleAxisData = [];
    var xAxisObj, gridObj, yAxisObj, seriesObj, singleAxisObj;
    var perGridHeight = realChartHeight / visualMLen, gridWidth = 0;
    var gridIndex = 0, left = LEFT_DISTANCE;
    /*
     * 为了避免label数量众多,而图表所占宽度不够时导致地标签重叠的情况发生,
     * 故需要对chartData和percentData的数据,结合realChartWidth和continuousKey进行二次计算
     * 以便图表得到最佳的展示结果,此时会人为剔除一些label标签,不会展示所有的label
    */
    var category = that.preprocessCategory(percentData, realChartWidth, continuousKey, chartData);
    var categoryIndex = category.categoryIndex;
    var disContinuousKey = that.getDisGroupItem(dLen, continuousKey);
    // 处理悬浮框的显示格式
    var formatter = that.curry(that.processTooltipFormatter, dimension, measure, disContinuousKey);     
    //分别对每一个度量进行遍历
    for (var j = 0; j < visualMLen; j++) {
        left = LEFT_DISTANCE;
        //对chartData图表数据进行遍历
        for (var i = 0, dataItem ; i < chartData.length; i++) {
             dataItem = chartData[i];
             //求出每一个分组的宽度大小
             gridWidth = isContinue ? perChartWidth : dataItem.count * perChartWidth;
             if (i >= 1) {
                 if (isContinue) {
                     left += perChartWidth;
                 } else {
                     left += chartData[i - 1].count * perChartWidth;
                 }
             }
             //grid对象的配置信息
             gridObj = {
                 width: gridWidth,
                 height: perGridHeight,
                 left: left,
                 top: (showClassNum  == 0 ? 14 : TOP_DISTANCE * showClassNum) + (j * perGridHeight + ((j > 0) &&  j * 10))
             };
             //x轴对象的配置信息
             xAxisObj = that.getXAxisOption({
                 type: dataItem.type,
                 mLen: mLen,
                 measureIndex: j,
                 axisPeak: dataItem.xAxisPeak,
                 realTimeFormat: dataItem.realTimeFormat,
                 timeFormat: dataItem.timeFormat,
                 axisLabel: dataItem.axisLabel,
                 categoryIndex: categoryIndex,
                 gridIndex: gridIndex,
                 gridWidth: gridWidth,
                 continueIndex: dataItem.continueIndex,                 
                 minInterval: dataItem.minInterval,
                 labelData: dataItem.axisLabelData,
                 index: i,
                 dimension:dimension
             });
             //y轴对象的配置信息
             yAxisObj = that.getYAxisOption({
                 index: i,
                 mLen: mLen,
                 context:  that,
                 measure: measure,                  
                 peakValue: peakValue,
                 gridIndex: gridIndex,
                 measureIndex: j
             });
             //series对象的配置信息
             seriesObj = that.getSeriesOption({
                 gridIndex: gridIndex,
                 measureIndex: j,
                 context:  that,
                 measure: measure,
                 xAxisType: dataItem.type,
                 xAxisKeyName: dataItem.xAxisKeyName,
                 xAxisKey: dataItem.xAxisKey,
                 state: state,                      
                 realTimeFormat: dataItem.realTimeFormat,
                 dateFormat: dataItem.dateFormat,
                 perChartWidth: perChartWidth,
                 barWidth: '20%',
                 color: color,
                 data: dataItem.data[j]
             });
             xAxisData.push(xAxisObj);
             yAxisData.push(yAxisObj);
             gridData.push(gridObj);
             seriesData.push(seriesObj);
             gridIndex++;
        }
    }
    var labelFormatWidth = [];
    //处理非坐标轴上的label的配置信息
    for (i = 0; i < percentData.length; i++) {
        labelFormatWidth[i] = realChartWidth / percentData[i].label.length - 10;
        labelFormatWidth[i] = labelFormatWidth[i] <= 26 ? 26 : labelFormatWidth[i];
        //获取singleAxisObj轴对象的配置信息
        singleAxisObj = that.getSingleAxisOption({
            index: i,
            mLen: mLen,
            realChartWidth: realChartWidth,
            realChartHeight: realChartHeight,
            distance: {
                topDistance: TOP_DISTANCE,
                rightDistance: RIGHT_DISTANCE,
                axisLabelHeight: AXIS_LABEL_HEIGHT,
                leftDistance: mLen === 0 ? 10 : LEFT_DISTANCE
            },
            labelFormatWidth: labelFormatWidth,
            percentData: percentData,
            labelData: category.category
        });
        singleAxisData.push(singleAxisObj);
        seriesObj = that.getSingSeriesOption({index: i});
        seriesData.push(seriesObj);
    }
    that.optionData = {
        xAxisData: xAxisData,
        yAxisData: yAxisData,
        gridData: gridData,
        formatter: formatter,
        legendData: legendData,
        singleAxisData: singleAxisData,
        seriesData: seriesData
    };
},  

getChartOption函数作为数据处理的总函数,是整个多维数据图表外部接口的核心,对于其中一些重要的子函数,本文会一一进行讲解。首先来看standarHigherData接口:
standarHigherData接口:该函数主要将原始的多维数据进行分解、变换、包装。如图1所示,为多维原始数据。如图2所示,为处理后的数据。
这里写图片描述

图1 多维原始数据

这里写图片描述
图2 经过standarHigherData接口处理后的数据

从上面两图中,可以看出处理后的数据和原始数据之间差异性还是很大的,做了很多变换、分解、包装。
该接口的具体源码如下所示:

/**
 * 用于规整高维数据,处理连续和离散情况下数据的内容和格式
 * @param {Array.<Array>} data: 需要分组的源数据
 * @param {Array.<Object>} measure: 包含度量信息的数组
 * @param {Array.<Object>} dimension: 包含维度信息的数组
*/
standarHigherData: function (data, measure, dimension) {
    var that = this, axisIndex;
    var continuousIndexs = [], groupedCategoryData = {};
    var dLen = dimension.length;
    var mLen = measure.length;
    var yAxisPeak = [], chartData = [], initChartData ;
    //获取连续的序号
    continuousIndexs = this.getContinueKey(dimension, mLen);
    //是否有连续型数据
    var isContinue = continuousIndexs.length > 0;
    // 当存在两个和以上的维度连续的情况下,需要对原始数据进行分组合计
    var groupData = this.groupByData(data, dLen, mLen, continuousIndexs);
    //只有一个维度连续或者没有维度连续的情况
    if (continuousIndexs.length <= 1) {
        groupedCategoryData[dLen - 1] = {};
        groupedCategoryData[dLen - 1].data = that.getDimensionCategory(groupData, dLen, mLen, isContinue);
        (continuousIndexs.length && mLen) && (groupedCategoryData[dLen - 1].xAxisPeak = getDimensionPeak(groupedCategoryData[dLen - 1].data.chartData, dimension, dLen - 1, that));
    //多个维度连续
    } else {
        for (var i = 0; i < continuousIndexs.length; i++) {
            var continuousItem = continuousIndexs[i];
            groupedCategoryData[continuousItem] = {};
            groupedCategoryData[continuousItem].data = that.getDimensionCategory(groupData[continuousItem], dLen - continuousIndexs.length + 1, mLen, isContinue);
            groupedCategoryData[continuousItem].xAxisPeak = getDimensionPeak(groupedCategoryData[continuousItem].data.chartData, dimension, continuousItem, that);
            var curYAxisPeakData = groupedCategoryData[continuousItem].data.peakValue;
            if (i == 0) {
                yAxisPeak = curYAxisPeakData;
            } else {
                for (var j = 0; j < mLen; j++) {
                    if (curYAxisPeakData[j].min < yAxisPeak[j].min) {
                        yAxisPeak[j].min = curYAxisPeakData[j].min;
                    }
                    if (curYAxisPeakData[j].max > yAxisPeak[j].max) {
                        yAxisPeak[j].max = curYAxisPeakData[j].max;
                    }
                }
            }
        }
    }
    //一个连续维度或者无连续维度
    if (continuousIndexs.length <= 1) {
        initChartData = groupedCategoryData[dLen - 1].data.chartData;
        for (var i = 0; i < initChartData.length; i++) {
            addAxisDataItem(groupedCategoryData, dimension, chartData, dLen - 1, i, that);
        }
        groupedCategoryData[dLen - 1].data.chartData = chartData;
        axisIndex = dLen - 1;
    //多个维度连续
    } else {
        initChartData = groupedCategoryData[continuousIndexs[0]].data.chartData;
        for (var i = 0, index; i < initChartData.length; i++) {
            for (var j = 0; j < continuousIndexs.length; j++) {
                index = continuousIndexs[j];
                addAxisDataItem(groupedCategoryData, dimension, chartData, index, i, that);
            }
        }
        groupedCategoryData[continuousIndexs[0]].data.chartData = chartData;
        groupedCategoryData[continuousIndexs[0]].data.peakValue = yAxisPeak;
        axisIndex = continuousIndexs[0];
    }
    return groupedCategoryData[axisIndex].data;
    /**
     * 每个数据项添加轴数据
     * @param groupedCategoryData
     * @param dimension
     * @param chartData
     */
    function addAxisDataItem(groupedCategoryData, dimension, chartData, index, dataIndex, context) {
        var gridData = {};
        var xAxisData = {};
        var key = dimension[index];
        var mLen = context.properties.measure.length;
        context.updateAxis('xAxis', xAxisData, key, null, 50);
        gridData = groupedCategoryData[index].data.chartData[dataIndex];
        echarts.util.merge(gridData, xAxisData);
        // ?! 量词对其后没有紧接着":"的"mm"字符串进行搜索
        gridData.realTimeFormat = gridData.realTimeFormat && gridData.realTimeFormat.replace(/m{2}(?!:)/g, 'MM');
        mLen == 0 && (gridData.type = 'category');
        gridData.xAxisKeyName = key.alias;
        gridData.xAxisKey = key.key;
        gridData.continueIndex = index;
        gridData.xAxisPeak = groupedCategoryData[index].xAxisPeak || null;
        chartData.push(gridData);
    }
    /**
       * 当轴连续的情况下,求出维度的最大值和最小值
       * @param {Array} chartData:图表x轴维度数据
       * @return {Object} 包含各个维度最大值和最小值的数组
    */
    function getDimensionPeak(chartData, dimension, index, context) {
        var totalData = [];
        var xAxisData = {};
        var key = dimension[index];
        context.updateAxis('xAxis', xAxisData, key);
        dateFormat = key.timeFormat || key.colType;
        if (xAxisData.type == 'category') {
            return;
        } else if (xAxisData.type == 'time') {
            for (var i = 0; i < chartData.length; i++) {
                totalData = totalData.concat(chartData[i].axisLabelData.map(function (item) {
                    item = (item === null || item == '' || item == 'null') ? '1970-01-01 00:00:00' : context.processTimeItem(dateFormat, item);
                    return new Date(item).getTime();
                }));
            }
        } else {
            for (var i = 0; i < chartData.length; i++) {
                totalData = totalData.concat(chartData[i].axisLabelData.map(function(item) {
                    return ((item === null || item == '' || item == 'null') ? 0 : item);
                }));
            }
        }
        var min = Math.min.apply(Array, totalData);
        var max = Math.max.apply(Array, totalData);
        return {
            min: min,
            max: max
        };
    }
},

在standarHigherData接口,也有几个重要的子函数,比如groupByData,主要是当连续的维度在两个和以上的时候,对数据进行分组合计,在这个过程有新的数据生成。
groupByData接口源码如下:

/**
  将数据进行分组,以'\&%#@'分割数据
    @param {Array} data: 需要分组的源数据
    @param {Number} dLen: 维度的个数
    @param {Number} mLen: 度量的个数
    @param {Array} groupDimenIndex: 需要分组的维度序号:[0, 1, 2]
    @return {Array} groupData: 分组后的数据
*/
groupByData: function(data, dLen, mLen, groupDimenIndex) {
    var inGroupIndex = getDisGroupItem(dLen, groupDimenIndex);
    var SPLIT_STR = '\&%#@';
    var key;
    var map = {};
    var dataItem;
    var groupDataMap = {};

    if (groupDimenIndex.length < 2) {
        return data;
    }
    for (var i = 0; i < groupDimenIndex.length; i++) {
        for (var j = 0 ; j < data.length; j++) {
            dataItem = data[j];
            key = getItemKey(dataItem, dLen, groupDimenIndex[i], inGroupIndex, SPLIT_STR);
            if (map[key]) {
                map[key] = sumMeasureItem(map[key], dataItem.slice(dLen, dLen + mLen));
            } else {
                map[key] = dataItem.slice(dLen, dLen + mLen);
            }
        }
        groupDataMap[groupDimenIndex[i]] = map;
        map = {};
    }
    return setMapToArray(groupDataMap);
    /**
       map的数据转为数据
       @param {Object} map
       @return 返回处理后的map
    */
    function setMapToArray(map) {
        var dimension = [];
        for (var prop in map) {
            if (map.hasOwnProperty(prop)) {
                for (var name in map[prop]) {
                    if (map[prop].hasOwnProperty(name)) {
                        dimension.push(name.split(SPLIT_STR).concat(map[prop][name]));
                    }
                }
                map[prop] = dimension;
                dimension = [];
            }
        }
        return map;
    }
    /**
     用于累加各个度量值
     @param {Array} curSum: 当前累加值
     @param {Array} addMeasure: 需要累加的度量值
        @return {Array} sum: 累加后的值
    */
    function sumMeasureItem(curSum, addMeasure) {
       var sum = [];
       for (var i = 0; i < curSum.length; i++) {
           sum.push(Number(curSum[i]) + Number(addMeasure[i]));
       }
       return sum;
    }
    /**
         获取分组的key值
         @param {Array} item:分组的单条数据
         @param {Number} groupDimenValue:分组字段的序号
         @param {Array} inGroupIndex:无需分组的序号
         @return {String} key: 分组的key值
    */
    function getItemKey(item, dLen, groupDimenValue, inGroupIndex, SPLIT_STR) {
        var key ;
        var keyIndex = inGroupIndex.concat([groupDimenValue]);
        for (var i = 0; i < dLen; i++) {
            if (keyIndex.indexOf(i) > -1) {
               key = key == null ? item[i] : (key + SPLIT_STR + item[i]);
            }
        }
        return key;
    }
    /**
       获取无需分组的字段序号
       @param {Number} dLen: 维度的个数
       @param {Array} groupDimenIndex: 需要分组的维度序号:[0, 1, 2]
       @return {Array} disGroupIndex: 无需分组的序号
    */
    function getDisGroupItem(dLen, groupDimenIndex) {
        var disGroupIndex = [];
        for (var i = 0; i < dLen; i++) {
            if (groupDimenIndex.indexOf(i) == -1) {
                disGroupIndex.push(i);
            }
        }
        return disGroupIndex;
    }
},

getDimensionPeak子函数,主要是当图表处于连续的情况下,求出各个连续维度的维度值的最大值和最小值,这样在各个分组中,才能对比连续性。

接着我们再来看preprocessCategory接口,
preprocessCategory接口:该接口主要对standarHigherData和calLabelProperty处理后的数据进行加工,筛选出在图表上不会互相重叠的标签label,最终经过这样筛选出的数据,才能显示在图表上。

/**
   * 根据图表宽度筛选各个维度上可以容纳的标签和标签显示所需要的属性信息
   * @param {Array} data: 标签数据
   * @param {Number} realChartWidth: 图表宽度
   * @param {Array} continuousKey:连续维度信息数组
   * @return {Object} 包含各个维度要显示的分组信息和坐标轴上显示的label内容
*/
preprocessCategory: function(data, realChartWidth, continuousKey, chartData) {
    var percentData = echarts.util.clone(data);
    var category = [];
    var width = 0;
    var percent, label, categoryItem, area;
    var lastLabelIndex;
    var interval = 1, classNum = percentData.length;
    var categoryIndex = [];
    var index = classNum - 1;
    var length = continuousKey.length;
    for(var i = 0; i < percentData.length; i++) {
        category[i] = {};
        percent = category[i].percent = [];
        label = category[i].label = [];
        area = category[i].area = [];
        categoryItem = percentData[i];
        for(var j = 0; j < categoryItem.percent.length; j++) {
            //前后两个标签是否重叠
            if (j == 0 || calLabelIndex(realChartWidth, categoryItem.percent, j, lastLabelIndex)) {
                percent.push(categoryItem.percent[j]);
                label.push(categoryItem.label[j]);
                area.push(categoryItem.area[j]);
                lastLabelIndex = j;
                //与坐标轴分类一致的类别才放入数组
                if (i == index) {
                   categoryIndex.push(j);
                }
            }
        }
    }
    //如果坐标轴上连续的维度key个数大于1
    if (length >= 2) {
        categoryIndex = calContinueAxisLabelIndex(chartData, length, realChartWidth);
    }
    //category是最终筛选出的分类数据,categoryIndex则是最靠近坐标轴的维度数据序号值
    return {category: category, categoryIndex: categoryIndex};
    /**
       计算前后两个label是否重叠
    */
    function calLabelIndex(realChartWidth, data, j, lastLabelIndex) {
        if (Math.round(realChartWidth * (data[j] - data[lastLabelIndex])) >= 26) {
          return true;
        }
        return false;
    }
    /**
     * 计算1个以上连续维度时,坐标轴的label显示情况
     * @param {Array} data :与坐标轴上最接近的维度标签数据
     * @param {Number} length: 连续维度的个数
         */
    function calContinueAxisLabelIndex(data, length, realChartWidth) {
        var categoryIndex = [];
        var chartBit = [];
              //计算出每一个坐标系的比率
        for (var i = 0, len = data.length; i < len; i++) {
            chartBit[i] = (i + 1) / len;
        }
        for(var j = 0; j < data.length; j++) {
            //前后两个标签是否重叠
            if (j == 0 || calLabelIndex(realChartWidth, chartBit, j, lastLabelIndex)) {
                lastLabelIndex = j;
                categoryIndex.push(j);
            }
        }
        return categoryIndex;
    }
},

processTooltipFormatter接口:该接口主要设置悬浮框的显示格式和内容,代码如下所示

        /**
           * 处理悬浮框显示的格式
           * @param {Array.<Object>} dimension: 包含维度信息的数组
           * @param {Array.<Object>} measure: 包含度量信息的数组
           * @param {Array} disContinuousKey:离散字段数组
           * @param {Object} params: echarts内部参数
        */
        processTooltipFormatter: function(dimension, measure, disContinuousKey, params) {
            var data = params.data;
            var len = data.length;
            var color = params.color;
            var formatter = '';
            var continueLen = dimension.length - disContinuousKey.length;
            var filterDimension = [];
            var timeFormat = params.timeFormat;
            var aliasIndex = 0;
            if (continueLen == 0) {
                //无连续字段
                filterDimension = dimension.filter(function (item, index) {
                    return index != (dimension.length -1)
                });
            } else {
                //有连续字段,并从dimension中删除,并得到新数组
                filterDimension = dimension.slice(0, disContinuousKey.length);
            }
            while (len - 2) {
                formatter += getFormatter(color, filterDimension[aliasIndex].alias, data[len - 1]);
                len -- ;
                aliasIndex ++ ;
            }
            formatter += getFormatter(color, params.xAxisKeyName, timeFormat ? new Date(data[0]).Format(timeFormat) : data[0]);
            measure.length > 0 && (formatter += getFormatter(color, measure[params.measureIndex].alias, data[1]));            
            return formatter;
            /**
               获取数据的html字符串
            */
            function getFormatter(color, name, value) {
                 return  ('<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:'
                           + color + '"></span>'+ name +':' + value + '</br>');
            }
        },

以上就是实现过程中的主要接口的介绍和阐释,还有一些小的功能和接口,由于比较简单,所以就没有介绍。还有本文将连续性图表和离散性图表分别进行了设计,但是在图表的实现过程中,由于两者有太多的相同处理步骤和方法,只是在一些细微处有一些差别。故为了提高代码的可复用性,在实现过程中,多维离散和连续图表的实现是放在一起的。

本人已将整个实现源码进行了封装,只要按文中的要求传入data、dimension、measure三个参数即可构造出图表。

demo源码下载地址:http://download.csdn.net/download/mulumeng981/9985030 (基于Echarts最新版3.7.1)

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值