使用 Chart.js 撸一个金融图表

使用 Chart.js 撸一个金融图表

引言

前些日子我需要为开发的软件添加一些数据图表,使我的程序数据可视化,经过一番查阅资料故选择了 Chart.js

Chart.js 概览

Chart.js 是一个非常受欢迎的开源库,在GitHub上有将近5万的 star 。它是轻量级的,能使用 HTML5 的 Canvas 元素构建响应式图表。可以轻松地对折线图和条形图进行混合和匹配以组合不同的数据集,这是非常棒的功能。它有8中类型的图表,能通过8种不同方式可视化数据;它们每个都具有动画性和可定制性。

这八种不同类型的图表分别是:Line、Bar、Radar、Doughnut & Pie、Polar Aear、Bubble、Scatter 和 Area 。当然你也可以组合一些图表使它们成为混合图表。

下面分别来介绍一下每种类型的图表:

Line

Description:折线图是在一条线上绘制数据点的方法。通常,它用于显示趋势数据或两个数据集的比较。

1.png

Bar

Description:条形图提供了一种显示以竖条表示的数据值的方式。有时用于显示趋势数据,并排比较多个数据集。

2_bar.png

Radar:

Description:雷达图是显示多个数据点及其之间变化的一种方式,它们通常可用于比较两个或多个不同数据集的点。

3_radar.png

Doughnut & Pie:

Description:饼图和甜甜圈图可能是最常用的图表。它们分为多个部分,每个部分的弧线显示每个数据的比例值。他们擅长显示数据之间的关系比例。饼图和甜甜圈图实际上是 Chart.js 中的同一类,但是具有一个不同的默认值-它们的 cutoutPercentage 。这等于应切掉内部的百分比。0对于饼图和50甜甜圈,默认为0。它们还在Chart核心中以两个别名注册。除了它们不同的默认值和别名之外,它们是完全相同的。下面只显示的 Pie 图。

4_doughnut and pie.png

Polar Area:

Description:极区图类似于饼图,但是每个线段具有相同的角度-线段的半径根据值而不同。当我们想要显示类似于饼图的比较数据,但还显示上下文值的刻度时,这种类型的图表通常很有用。

5_polar area.png

Bubble Chart:

Description:气泡图用于同时显示三个维度的数据。气泡的位置由前两个维度以及相应的水平和垂直轴确定。第三维由各个气泡的大小表示。

6_bubble chart.png

Scatter Chart:

Description:散点图基于x轴变为线性轴的基本折线图。要使用散点图,必须将数据作为包含X和Y属性的对象进行传递。下面的示例创建一个包含3点的散点图。

Area Charts:

Description:两个线和雷达的图表支持 fill 其可用于两个数据集或数据集和边界之间创建区域数据集对象,即,刻度上的选项 origin ,start 或 end 。

附:Mixed Chart Types:

Description:使用Chart.js,可以创建混合图表,这些图表是两个或多个不同图表类型的组合。一个常见的示例是条形图,其中还包含折线数据集。

Chart.js 使用

首先我们需要安装它的核心 Chart.js 。

方法一:如果你在电脑上部署了 NodeJs 环境,则可直接通过 npm install chart.js --save 来直接下载它。如果你安装了 bower 则可直接通过 bower install chart.js --save 来下载它。

方法二:可以通过 CDN 的方式来引用它:国内七牛云的 https://cdn.staticfile.org/Chart.js/3.0.0-alpha/Chart.min.js 国外 https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js

方法三:也可以通过 GitHub 来直接获取。

选择你想要的包:

Chart.js 提供了两种不同类型的包:单独的 Chart.js、Chart.min.js 包 和 捆绑的 Chart.bundle.js、Chart.bundle.min.js 包。捆绑的版本在单个文件中包含 Moment.js 。如果需要时间轴并且要包含一个文件,则应使用此版本。如果应用程序已包含 Moment.js ,则不应使用此版本,不然将两次包含 Moment.js ,这将导致页面加载时间增加还有可能有版本兼容性问题。捆绑版本中的 Moment.js 版本是 Chart.js 私有的,因此,如果想自己使用 Moment.js ,最好使用Chart.js(非捆绑)并手动导入 Moment.js。

引用的方式:

方法一:通过 <script src="xx.js"></script> 标签的方式引入。

方法二:通过 require('xx.js') 函数引入。

方法三:通过 Import x from ‘xx.js’ 的方式从外部导入。

它的用法:

要创建图表,我们需要实例化 Chart 该类。为此,我们需要传入要绘制图表的画布的节点,jQuery实例或2d上下文。这是一个例子。

<canvas id="myChart" width="400" height="400"></canvas>

var ctx = document.getElementById('myChart');

or var ctx = document.getElementById('myChart').getContext('2d');

or var ctx = $('#myChart');

or var ctx = 'myChart';

有了元素或上下文后,就可以实例化预定义的图表类型或创建自己的图表类型了!

常规配置:

这些部分描述了可在文档中其他位置应用的常规配置选项。

响应式定义了适用于所有图表的响应式图表选项。

设备像素比率定义显示像素与渲染像素之间的比率。

交互定义了反映悬停图表元素如何工作的选项。

选项可脚本化和可索引的选项语法。

颜色定义可接受的颜色值。

字体定义各种字体选项。

性能为对性能敏感的应用程序提供了技巧。

【 Chart.js 操作图表

更新图表:

创建图表后,通常要 update() 图表。更改图表数据或选项时,Chart.js 将为新数据值和选项设置动画。

添加/删除数据:

通过更改数据数组来支持添加和删除数据。要添加数据,只需将数据添加到数据数组中即可,如本示例所示。

function addData(chart, label, data) {
    chart.data.labels.push(label);
    chart.data.datasets.forEach((dataset) => {
        dataset.data.push(data);
    });
    chart.update();
}

function removeData(chart) {
    chart.data.labels.pop();
    chart.data.datasets.forEach((dataset) => {
        dataset.data.pop();
    });
    chart.update();
}

更新选项:

要更新选项,支持在适当位置更改options属性或传入新的options对象。要更新选项,支持在适当位置更改options属性或传入新的options对象。

如果选项被适当改变,其他选项属性将被保留,包括由Chart.js计算的属性。如果选项被适当改变,其他选项属性将被保留,包括由Chart.js计算的属性。

如果将其创建为新对象,则类似于使用选项创建新图表-旧选项将被丢弃。如果将其创建为新对象,则类似于使用选项创建新图表-旧选项将被丢弃。

禁止动画:

有时当图表更新时或者为了提高性能,您可能不需要动画。要实现此目的,您可以将 update 时间设置为0,它将同步呈现图表,并且不会产生动画效果动画。

实战 Chart.js

环境:原生 Javascript 。首先为了快速实现项目和避免不必要的麻烦,这里我选择的是 Chart.bundle.min,js 包,我们将这个包下好后 copy 到我们的项目工程中的相应的文件去。我们来到要使用图表的 HTML 页面在 <body> 标签最下面通过 <script src="xx.js"></script> 标签引入这个文件,随后我们在相应的 js 中实例化它 var xxx = document.getElementById('xxx').getContext('2d');,然后我们在要显示图表的标签内创建 <canvas id="xxx"></canvas>

然后我们可以创建相应的图了,比如我这里创建了折线区域图和饼状图:

//折线区域图 Boundaries
const canvasBoundaries = document.getElementById('contentBillOverViewPageDataBoundaries').getContext('2d');

mycanvasBoundaries = new Chart(canvasBoundaries, {
	type: 'line',
	data: {  
        labels: [0,0,0,//自己添加...],  
        datasets: [{
            label: 'x年x月财务数据',  
            data: [0,0,0,//自己添加...],  
            backgroundColor: [  
                'rgba(255, 99, 132, 1)',  
                'rgba(54, 162, 235, 1)',  
                'rgba(255, 206, 86, 1)',  
                 //根据喜好自己添加...
            ],  
            borderColor: [  
                'rgba(255, 99, 132, 1)',  
                'rgba(54, 162, 235, 1)',  
                'rgba(255, 206, 86, 1)',  
                 //根据喜好自己添加...
            ],  
            borderWidth: 0  
        }]  
   },  
	options: { 
		title: {  
			display: false,  
			text: '近日财务数据',  
			fontSize:15,  
			fontColor: "#000",  
		},
		//responsive: false ,//取消响应式,避免canvas溢出容器。
		legend: {onClick: function () {}}
	}
});
		
//饼状图 Doughnut
const canvasDoughnut = document.getElementById("contentBillOverViewPageDataDoughnut").getContext('2d');

mycanvasDoughnut = new Chart(canvasDoughnut, {
	type: 'pie',
	    data: {
	        labels: ['工资类','食材类','物耗类'],  
			datasets: [{  
	            data: [1,1,1],
	            backgroundColor: ['#debd5a', '#3cc9bf', '#ff6d4a']
	        }]
	    },
	options: {
		title: {  
			display: false,  
			text: '财务概览',  
			fontSize:15,  
			fontColor: "#000",  
		},  
		responsive: false ,//取消响应式,避免canvas溢出容器。
	}
});

做完上述工作之后,我们就可以通过相应的 js 文件向 worker.js 发起请求,再由 worker.js 发送向服务器带有 JSON 数据的 HTTP 请求 ,服务器处理数据然后返回相应的 JSON 数据。//饼状图代码略

xx.js文

//...
//异步获取当月的折线区域图账单
function asyncGetLineConditionBillData(){
	worker.onmessage = e => {
		const result = e.data;
		var dataJson = result['data'];
		if(result.fg == 0){
			plus.nativeUI.toast("异常登录,请联网重试");
			return false; //异常
		}
		//获取当前月份的天数
		var getMonthDay = new Date(conditionYear,conditionMonth,0);
		var getMonthDayCount = getMonthDay.getDate();
		var getDayMoney = 0;
		//遍历每天的数据,存入array中
		everyDayCount = [];
		everyDayMeony = [];
		for(var i=1;i<=getMonthDayCount;i++){
			//生成当月的每个天数
			everyDayCount.push(conditionMonth+"-"+i);
			//生成每日的Meony为均0
			everyDayMeony.push(0);
		}
		//遍历每天的收支总和
		for(var n in dataJson){
			var regDay = parseFloat(/^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(dataJson[n].dt)[3]);//正则处理和获取日期
			if(dataJson[n].td == 'p'){//p
				//计算当日的Meony值
				everyDayMeony[regDay-1] = everyDayMeony[regDay-1] - parseFloat(dataJson[n].ta);
				//删除当前元素
				//delete dataJson[n];
			}else if(dataJson[n].td == 'e'){//e
				//计算当日的Meony值
				everyDayMeony[regDay-1] = everyDayMeony[regDay-1] + parseFloat(dataJson[n].ta);
				//删除当前元素
				//delete dataJson[n];
			}
		}
		//加载折线区域图
		nowMonthBoundaries(mycanvasBoundaries,conditionYear,conditionMonth,everyDayCount,everyDayMeony);
	}
	//检查flag位
	checkFlag();
	//提交到后台的线程进行处理
	var accountValue = localStorage.getItem("at");
	var status = 1;
	worker.postMessage(['asyncGetLineConditionBillData',accountValue,conditionYear,conditionMonth,status]);
	
}
//...

worker.js

//...
//异步获取折线区域图条件账单
if(message[0] == "asyncGetLineConditionBillData"){
	//检查状态是否为1
	if(message[4] !== 1 ){
		return false;
	}
	fetch('http://www.xx.cn/xx', {
		headers: {
			'Content-Type': 'application/json'
		},
		mode: 'cors',
		method: 'POST',
		body: JSON.stringify({
			"accountValue" :message[1],
			"conditionYear" :message[2],
			"conditionMonth":message[3]
		}),
	}).then(function(response){
		return response.text();
	}).then(function(text){
		returnArray =  JSON.parse(text);
		postMessage(returnArray);
	}); 
}
//...

然后来到我们的服务器端,编写相应的 Router 到 相应的 Controller 的代码:

router.php

//..
//获取交易折线区域数据
Route::post('/xx','xx@xx');
//..

xx.controller.php

//..
namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Http\Request; // 接收参数
public function AdminGetLineConditionBillData(Request $request){

    $contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
    //判断传递的类型
    if ($contentType != "application/json") {
        $returnArray = array('fg' => 0 );//异常
        return json_encode($returnArray,JSON_UNESCAPED_UNICODE);
    }
    $accountValue = $request -> json('accountValue');
    $conditionYear = $request -> json('conditionYear');
    $conditionMonth = $request -> json('conditionMonth');
    //检查传递的数据是否为空
    if($accountValue == ""){
        $returnArray = array('fg' => 0 );//异常
        return json_encode($returnArray,JSON_UNESCAPED_UNICODE);
    }
    $accountInfo = DB::connection('xx') ->table('xx') 
                                         ->select('id','at','td','td','dt')
                                         ->whereYear('dt',$conditionYear) 
                                         ->whereMonth('dt',$conditionMonth)
                                         ->get();
    DB::disconnect('xx');
    if($accountInfo ->isEmpty()){
        $returnArray = array('fg' => 1 );//当前查询无数据
        return json_encode($returnArray,JSON_UNESCAPED_UNICODE);
    }else{
        $returnArray = [];//创建空的数组
        foreach ($accountInfo as $value) {
            if($value ->td == "p"){
                $billTypeDb = "xx";
            }else{
                $billTypeDb = "xx"; 
            }
            $accountInfoTable = 'b'.$value ->at;
            $billTypeTable = $value ->td ."_". $value ->id; //交易类型表名
            $result = DB::table("xx.pe")
                ->select("xx.xx.td","xx.xx.dt","{$billTypeDb}.{$billTypeTable}.ta")
                ->join("{$billTypeDb}.{$billTypeTable}","{$billTypeDb}.{$billTypeTable}.at",'=',"xx.xx.at")
                ->where('xx.xx.id',$value ->id)
                ->get();
            if($result ->isEmpty()){
                continue;
            }else{
                array_push($returnArray,...$result);
                //$returnArray[] = $result; 
                //最好使用$array[]=,因为这样可以避免调用函数的开销,如果创建则不用考虑。
            }
        }
        $returnArray = ['data' => $returnArray]; 
        return json_encode($returnArray,JSON_UNESCAPED_UNICODE);
    }
}
//..

服务器返回相应的 JSON 数据后,再由之前的 xx.js 接受 worker.js 的回调拿到相应的数据,进行解析的操作,解析完成后再把相应的数据传递给图表然后使用 update() 函数进行更图表,由此我们就能看到和实现想要的效果了。

 //折线区域图
function nowMonthBoundaries(mycanvasBoundaries,conditionYear,conditionMonth,everyDayCount,everyDayMeony){
	mycanvasBoundaries.data.labels = everyDayCount;
	mycanvasBoundaries.data.datasets[0].label = conditionYear+'年'+conditionMonth+'月财务数据';
	mycanvasBoundaries.data.datasets[0].data = everyDayMeony;
	mycanvasBoundaries.update(); //更新折线区域图
}

实现结果:

7_result.png

完结

撒花~花花


回复列表



回复操作

正在加载验证码......

请先拖动验证码到相应位置

发布时间:2020-05-15 01:55:09

修改时间:2020-05-15 01:55:09

查看次数:38

评论次数:0