Salesforce Lightning组件中使用Chart.JS实现数据可视化(二)


上篇博客提到,,并以Line Chart完成了一个案例,接下来我们进一步优化这个案例

之前的博客中,ChartJS显示的画面如下,

在这张表当中,很显然,2014与2020对应的两个数据点并不能将数值显示完全,同时可以看到,2020的最高点被遮住了一部分,对应的数字更是没有显示出来,所以我们修复下这几个问题:

  • 最高点显示数据不完整
  • 最左边更最右边的数据显示不完整

同时需要优化的地方包括:

  • 显示的数字因为是货币,所以给加上对应的货币分隔符
  • 动态显示画布的大小

问题1:最高点显示数据不完整

数值最大的点显示不完整,其本质上是这个数值达到了Y轴的最高点,所以,解决方法就是设置Y的高度始终高于数据集中的最大值,而考虑到数据的量级可能是个位数,又或者百万甚至千万,直接设置Y轴的大小显然并不可取,那么我们可以将Y轴最大值 = 数据集最大值 * 130%,这样动态的保证数据点用于囊括在Y轴里面,从而确保了最高点不会被遮住,那么上代码:

// 取出数据集中的最大值
var MULTIPLE = 1.3; // 设置Y轴 在最大值的基础上延长多少
var maxValue = temp[0]["firstValue"];
        
for(var a=0; a< temp.length; a++){
  label.push(temp[a]["label"]);
  firstValue.push(temp[a]["firstValue"]);
  secondValue.push(temp[a]["secondValue"]); 
  if(maxValue < temp[a]["firstValue"]) maxValue = temp[a]["firstValue"];
  if(maxValue < temp[a]["secondValue"]) maxValue = temp[a]["secondValue"];
}    
// 设置Y轴参数
yAxes: [{
    gridLines: {display:false},
    display: false, // 隐藏Y轴
    ticks: { // 设置Y轴最大最小坐标点
        min: 0, 
        max: maxValue * MULTIPLE,
    },
}]

展示效果:

问题2:最左边更最右边的数据显示不完整

X轴左右两边的数据点显示不完整,根据问题一的结果,第一反应毫无疑问是延长X轴线来解决问题,但是在查阅了相关文档后,并没有合适的,可以用来操作的空间,所以常规手段,将最左边的点往右挪,最右边的点往左挪,不过这种情况就需要注意,当数据点仅有一个时该如何显示,话不多说,上代码

animation:{
    "duration":1,
    "onComplete": function() {
        var chartInstance = this.chart,
        ctx = chartInstance.ctx;
        ctx.fillStyle = 'black';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'bottom';
        this.data.datasets.forEach(function(dataset, i) {
            var meta = chartInstance.controller.getDatasetMeta(i);
            var len = meta.data.length;
            meta.data.forEach(function(bar, index) {
                var data = '$' + dataset.data[index];
                var Offset = data.length * 2; // 偏移量
          // 注意,Y轴上 - 5是向上移动 if(len >= 2){ if(index == 0){// 第一个数据点的偏移 ctx.fillText(data, bar._model.x + Offset, bar._model.y - 5); }else if(index == len -1){ // 最后一个点的偏移 ctx.fillText(data, bar._model.x - Offset, bar._model.y) - 5; }else{ ctx.fillText(data, bar._model.x, bar._model.y - 5); } }else{// 当只有一个数据点的时候 ctx.fillText(data, bar._model.x + Offset, bar._model.y - 5); } }); }); } }

这里面我引入了一个偏移量的概念,原理同问题一,数据量级的问题,个位数和百位千位十万位需要挪动的像素大小是完全不一样的,所以我根据当前数据本身的长度,分别向左右各偏移两倍的长度,这样显示的效果在各种量级下自然没有差别,当然你喜欢也可以自己设定合适的大小

展示效果:

 做到这里,为了测试数据展示的效果,我尝试只传入一个点的时候发现一个问题,

 可以看到,当只有一条数据时,Line类型的图表会将数据点显示在Y轴上,虽然数据显示没问题,但是在展示的时候,会显得非常奇怪,相对来说,将这个点移动到图表的中间效果更合适,但是在我翻遍文档后,发现这一点并不能通过配置来实现,所以只能另想办法,最后我将只有一个点的时候,传入两个空值,在前端显示时,隐藏空值的点,这样根据三个点的显示效果,这唯一一个数据点就会出现在正中间了,那么怎么做呢?

首先,ChartJS中,坐标点传入 null值是不会显示的,所以利用这一点,我们在第一个点的位置前后各传递一个null,因为 LineChartVar下的fisrtValue和secondValue都是Integer的数据,所以假设传入值为-1,在Aura组件的JS文件中将-1替换成null即可

Controller & Helper 核心代码:

//  此处仅作为展示,假设数据不包括 -1 ,实际项目中应改成其他值,因为LineChartVar的中firstValue都是整数类型,而Integer类型不支持传null进去,
// 所以我设置值为-1,在JS中将-1替换掉
List myLineChartVarList = new List(); myLineChartVarList.add(new LineChartVar('ISNULL', -1, -1)); myLineChartVarList.add(new LineChartVar('2014', 100,120)); myLineChartVarList.add(new LineChartVar('ISNULL', -1, -1)); return JSON.Serialize(myLineChartVarList);
var MULTIPLE = 1.3; // 设置Y轴 在最大值的基础上延长多少
// 注意 -1 是假设的的值,所以最大值不能取
var maxValue = temp[0]["firstValue"] == -1 ? 0 : temp[0]["firstValue"]; 

for(var a=0; a < temp.length; a++){
    //  label为ISNULL说明是我们传入的占位点,所以将Label设置为''
    var tempLabel = temp[a]["label"] == 'ISNULL' ? '' : temp[a]["label"];
    if(tempLabel == '') MULTIPLE = 2;
    label.push(tempLabel);
    
    //  value为-1时,说明是传入的占位符,所以将其设定为null
    var tempValue = temp[a]["firstValue"] == -1 ? null : temp[a]["firstValue"];
    firstValue.push(tempValue);

    var tempValue2 = temp[a]["secondValue"] == -1 ? null : temp[a]["secondValue"];
    secondValue.push(tempValue2);

    // setting the yAxes ticks 
    var FValue = temp[a]["firstValue"] == -1 ? 0 : temp[a]["firstValue"];
    var SValue = temp[a]["secondValue"] == -1 ? 0 : temp[a]["secondValue"];
    if(FValue > maxValue) maxValue = FValue; // get MaxValue
    if(SValue > maxValue) maxValue = MValue; // get MaxValue
}

为了方便查看演示效果,我将X轴的网格线重新打开,可以看到,现在的唯一的数据点就被放在了中间位置,效果比起默认的情况就好很多了

 然后我们继续最开始的优化,想要让数据点显示货币分隔符号,其实只需要在HelpJS中对数据进行简单的处理就好

meta.data.forEach(function(bar, index) {
    var tempdata = dataset.data[index];
    // 数据点的值有可能为null,所以先加上判断
    if(tempdata != null){
        // 考虑传入的数据不一定全是整数,当传入一个浮点数时,我们以 "." 进行分隔
        var arry = tempdata.toString().split(".");
        // split分隔出来的数组,第一个数字一定是整数部分,直接通过正则表达式对其添加货币分隔符号
        tempdata = arry[0].toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');
        // 如果是浮点数,那么 split分隔的数组就有两个值,第二个值就是小数部门,直接拼接即可
        if(arry.length > 1) tempdata = tempdata + '.' + arry[1];
    }
    // 这里添加 “$” 是为了避免 null 值时,data.length 这个报错,毕竟空指针问题
    var data = '$' + tempdata;
    
    // var data = '$' + dataset.data[index];
    var Offset = data.length * 2;
    .......
}

查看显示效果:

看上去还不错,分隔符的显示很完美,那么接下来最后一个优化,让画布的大小自适应,这个其实比较简单,上一篇提到,改变画布的大小有两种方式:

// 方式一:在Helper中直接渲染
var el = cmp.find('lineChart').getElement();
var ctx = el.getContext('2d');
ctx.canvas.width = '700';
ctx.canvas.height = '300';

// 方式二:在cmp中,直接设定宽高

要使画布大小自适应,那么显然在Helper中直接渲染是可行的,所以我们获取画布外的div大小,然后将div的宽高赋值给画布,就能保证在画布渲染出来时,是符合div大小的,所以,代码如下:

// 获取容器div的大小
var divChart = document.getElementById('chartContainer');

var el = cmp.find('lineChart').getElement();
var ctx = el.getContext('2d');
// 将容器div的大小赋值给画布
ctx.canvas.width = divChart.offsetWidth;
ctx.canvas.height = divChart.offsetHeight;

将div的宽度设置为100%,高度设置为50%


    

演示效果如下:

到这里为止,针对ChartJS数据展过程中的优化点做了更细致的整理和解决,希望对看到这里的你有所帮助

最后,友情提示,ChartJS的版本是在不断迭代的,如果你看到有的代码你没法运行,或者运行的展示结果并不一样,那么我推荐你换一个版本过来进行尝试,比如在ChartJS中对tooltip的样式属性:displayColors,borderColor,borderWidth这些,都有提供自定义选项,但是如果版本不够新,这些属性并不能正常工作,所以在使用ChartJS时,你使用的版本也很重要

参考内容:

ChartJS中文文档

ChartJS演示案例

ChartJS官方文档

附上完整的Helper Controller代码

({
    createLineGraph : function(cmp, temp) {
        var label = [];
        var firstValue = [];
        var secondValue = [];
        
        var MULTIPLE = 1.3; // 设置Y轴 在最大值的基础上延长多少
        // 注意 -1 是假设的的值,所以最大值不能取
        var maxValue = temp[0]["firstValue"] == -1 ? 0 : temp[0]["firstValue"]; 

        for(var a=0; a < temp.length; a++){
            //  label为ISNULL说明是我们传入的占位点,所以将Label设置为''
            var tempLabel = temp[a]["label"] == 'ISNULL' ? '' : temp[a]["label"];
            if(tempLabel == '') MULTIPLE = 2;
            label.push(tempLabel);
            
            //  value为-1时,说明是传入的占位符,所以将其设定为null
            var tempValue = temp[a]["firstValue"] == -1 ? null : temp[a]["firstValue"];
            firstValue.push(tempValue);
        
            var tempValue2 = temp[a]["secondValue"] == -1 ? null : temp[a]["secondValue"];
            secondValue.push(tempValue2);
        
            // setting the yAxes ticks 
            var FValue = temp[a]["firstValue"] == -1 ? 0 : temp[a]["firstValue"];
            var SValue = temp[a]["secondValue"] == -1 ? 0 : temp[a]["secondValue"];
            if(FValue > maxValue) maxValue = FValue; // get MaxValue
            if(SValue > maxValue) maxValue = SValue; // get MaxValue
        }
        
        var divChart = document.getElementById('chartContainer');
        
        var el = cmp.find('lineChart').getElement();
        var ctx = el.getContext('2d');
        ctx.canvas.width = divChart.offsetWidth;
        ctx.canvas.height = divChart.offsetHeight;
        
        var data = {
            labels: label,
            datasets: [{
                label: 'USD Sent',
                data: firstValue,
                backgroundColor: "rgba(153,255,51,0.4)",// 填充的颜色
                pointRadius:5,
                pointStyle:'circle',
                borderColor:'#bbd9b7',// 折线的颜色设置
                pointBorderColor:'#00ff00', // 顶点圈圈的颜色
                pointBackgroundColor:'#00ff00' // 顶点的颜色
            }, {
                label: 'USD Recieved',
                data: secondValue,
                backgroundColor: "rgba(255,153,0,0.4)",// 填充的颜色
                pointRadius:5,
                pointStyle:'circle',
                borderColor:'#bbd9b7',// 折线的颜色设置
                pointBorderColor:'#ff0000', // 顶点圈圈的颜色
                pointBackgroundColor:'#ff0000' // 顶点的颜色
            }]
        };
        
        var options = {
            elements: {
                line: {tension: 0} // 设置折线图
            },
            legend: {display: false}, // 设置不显示图例
            responsive: false, // responsive和maintainAspectRatio设置为false才可以调整图标的宽高
            maintainAspectRatio: false,
            scales: {
                xAxes: [{
                    gridLines: {display:false} // 隐藏网格线
                }],
                yAxes: [{
                    gridLines: {display:false},
                    display: false, // 隐藏Y轴
                    ticks: { // 设置Y轴最大最小坐标点
                        min: 0, 
                        max: maxValue * MULTIPLE,
                    },
                }]
            },
            // 设置显示数据的顶点
            hover:{animationDuration:0},//防止鼠标移动到顶点时的闪烁效果
            animation:{
                "duration":1,
                "onComplete": function() {
                    var chartInstance = this.chart,
                        ctx = chartInstance.ctx;
                    ctx.fillStyle = 'black';
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'bottom';
                    this.data.datasets.forEach(function(dataset, i) {
                        var meta = chartInstance.controller.getDatasetMeta(i);
                        var len = meta.data.length;
                        meta.data.forEach(function(bar, index) {
                            var tempdata = dataset.data[index];
                            if(tempdata != null){
                                var arry = tempdata.toString().split(".");
                                tempdata = arry[0].toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');                                
                                if(arry.length > 1) tempdata = tempdata + '.' + arry[1];
                            }
                            var data = '$' + tempdata;
                            
                            // var data = '$' + dataset.data[index];
                            var Offset = data.length * 2;
                            var Offset = data.length * 2; // 偏移量
                            if(len >= 2){
                                if(index == 0){//first point style 
                                    ctx.fillText(data, bar._model.x + Offset, bar._model.y - 5);    
                                }else if(index == len -1){ // last point style
                                    ctx.fillText(data, bar._model.x - Offset, bar._model.y - 5);    
                                }else{
                                    ctx.fillText(data, bar._model.x, bar._model.y - 5);    
                                }
                            }else{// only one point style
                                ctx.fillText(data, bar._model.x + Offset, bar._model.y - 5);    
                            }
                        });
                    });
                }
            }
        };
        
        new Chart(ctx, {
            type: 'line',
            data: data,
            options: options
        });
        
    }
})