JavaScript Canvas 实现自定义多折线图
大家好,我是梦辛工作室的灵,今天给大家讲一讲如何实现一个好看的自定义多折线图,按惯例来,先看实现效果如下:
画面效果还是可以吧,颜色和内容都可以自定义哦,给大家讲一下我的绘制逻辑,我们需要将绘图封装成一个对象,并先给定一些默认值,比如画布的宽和高,我们将画布分成上下两部分,定义上面30%用于绘制标题和留出一些空白,图表占用60%,剩下的10%留白,用于显示横坐标的数值,且这样好看点:
还要 提供一个 装数据的对象,我这样定义的该对象:
title会在中间标题那里显示,并按照对应的的颜色值color,data里面放的就是图表的数据了,并对外提供set 方法便于外部修改数据内容,再设置时需要对所有数据进行排序并获取到数据中的 横纵坐标的最大值和最小值,等下我们计算坐标的时候会用到
接下来就是开始绘图了,将绘图操作放至 同一个绘图函数:
画图前先清空画布,防止多次画图,然后设置当前的样式信息,开始填充左侧标题的文本,顶部标题的文本,图表上方的单位 文本,顶部的划线信息,接下来就是绘制图表内容
设置线的颜色和宽度,将画笔移动到 图表的左上角,即x是 图表的左边距 y 是图表的上边距,然后绘制到
x是 图表的左边距 y 是 图表的上边距 加上图表的高度,继续话横坐标线,先计算每个横坐线的间隔高度,
然后按照每个高度进行画横线; 然后绘制最大值和最小值的显示;接下来就是对每个点开始绘制了,这里得先提供几个方法,就是 根据 x 坐标 计算出当前 横坐标应对应的数据,根据数据 计算出 当前的 x 坐标, 根据 y 坐标 计算出 当前纵坐标对应的 数据,根据数据计算出 当前的 y 坐标;大致的 计算 方法 就是 用 最大值 和 最小值之差 除以 最大坐标值 和 最小坐标值 之差 ,获得一个对应的比例,然后用当前的坐标值乘以这个比例 在加上 最小值 ,就可以得到当前坐标值对应的数据值,具体如下:
这样的话 ,等下我们数据的折线就简单了,直接 提交 数值 就可以得到对应的 坐标值,画图就行
最后还有一个 小弹窗的绘制,这个就需要监听鼠标移动事件,并判断是否在 图表里,若在允许绘制弹窗,不在则不允许绘制图表:
判断鼠标是否在图表内
然后在draw方法的最后面执行一个 绘制弹窗的操作
这里我增加了一个判读,当弹窗要超出界线时,将弹窗修改绘制方向,
最后我放上所有的源代码,供大家参考,如有不对之处,还请大家指出
先是html文件:
<!doctype html>
<html>
<!DOCTYPE >
<html>
<head>
<meta http-equiv="Content-Type" content="text/html,charset=utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
</head>
<body>
<div id="Contents" >
<div style="width:100%;height:auto;margin-top:40px;text-align: center;">
<canvas id="myCanvas" width="900" height="600" style="background:#fff;margin-top:10px;border:1px solid #b0b0b0;">您的浏览器不支持 HTML5 canvas 标签。</canvas>
</div>
</div>
<script type="text/javascript" src="chart.js"></script>
<script type="text/javascript" src="index.js"></script>
</body>
</html>
然后是 index.js:
window.onload = (argument) => {
var chartutil = new drawChart({
width:900,
height:600,
canvasid:"myCanvas"
});
chartutil.setDataInfo([]);
chartutil.draw();
}
然后是chart.js:
/**
* 梦辛工作室
* 灵 v1.0.1
*/
function drawChart(options) {
var canvasid = options.canvasid; //画布id
var width = options.width || 800; //画布宽 默认值 800
var height = options.height || 400; //画布高 默认值 400
var chartLeft = width * 0.1; //图表的左边距
var chartTop = height * 0.3; //图表的上边距
var widthChart = options.width * 0.8; //图表宽度
var heightChart = options.height * 0.6; //图表高度
var maxY = 100; //默认众坐标最大值
var minY = 0; //默认众坐标最小值
var maxX = 1000; //默认横坐标最大值
var minX = 0; //默认横坐标最小值
var lineNunber = 7; //分成几条线
var unitstr = options.unit || "单位(°)"; //单位显示
var unitFontSize = 12; //单位显示 字体大小
var lineShortWidth = 5; //横线的宽度
var leftTitle = options.leftTitle || "左边标题"; //图标表左边的标题
var title = options.title || "中间标题"; //图表中间标题的内容
var leftTitleFontSize = 15; //图表左边标题的文字大小
var numberFontSize = 10; //图表数字的文字大小
var titleFontSize = 16; //中间标题的文字代销
var datatitleFontSize = 13; //每条线的标题大小
var dataTitleCirclePath = 10; //每条线的标题小圆的直径
var startLineCirclePath = 8; //画点的时候 大圆的直径
var endLineCirclePath = 4; //画点的时候 小圆的直径
var isShowDialog = false; //是否显示 小弹窗
var curMouseX = 0; //当前鼠标的X坐标 相对于画布
var curMouseY = 0; //当前鼠标的Y坐标 相对于画布
var c = document.getElementById(canvasid); //获取到canvas 对象
c.onmousemove = canvsMouseMoveLister; //监听鼠标移动事件
c.onmouseout = canvsMouseLeave; //监听鼠标的移出事件
var ctx = c.getContext("2d");
var date = new Date();
var dataInfo = [
{
title:"X(°)",
color:"#FF0000",
data:[{key:20,value:20},{key:30,value:2},{key:50,value:10},{key:70,value:15},{key:80,value:18},{key:100,value:20},{key:300,value:20},{key:500,value:0},{key:600,value:80}]
},
{
title:"Y(°)",
color:"#00FF00",
data:[{key:20,value:10},{key:30,value:5},{key:50,value:100},{key:70,value:14},{key:80,value:19},{key:100,value:30},{key:300,value:50}]
},
{
title:"Z(°)",
color:"#0000FF",
data:[{key:20,value:5},{key:30,value:50},{key:50,value:10},{key:70,value:90},{key:80,value:80},{key:100,value:60},{key:300,value:50}]
},
];
var _self = this;
this.init = function(options){
canvasid = options.canvasid;
width = options.width || 800;
height = options.height || 400;
chartLeft = width * 0.1;
chartTop = height * 0.3;
widthChart = options.width * 0.8;
heightChart = options.height * 0.6;
maxY = 100;
minY = 0;
maxX = 1000;
minX = 0;
lineNunber = 7; //分成几条线
unitstr = options.unit || "单位(°)";
unitFontSize = 12;
lineShortWidth = 5;
leftTitle = options.leftTitle || "倾角计-1";
title = options.title || "采样频率:暂未记录 上报频率:暂未记录 加报频率:暂未记录";
leftTitleFontSize = 15;
numberFontSize = 10;
titleFontSize = 16;
datatitleFontSize = 13;
dataTitleCirclePath = 10;
startLineCirclePath = 8;
endLineCirclePath = 4;
c = document.getElementById(canvasid);
c.onmousemove = canvsMouseMoveLister;
c.onmouseout = canvsMouseLeave;
ctx = c.getContext("2d");
}
this.setDataInfo = function(dataInfos){
var isSet = {};
for (var x in dataInfos) {
var len = dataInfos[x].data.length;
dataInfos[x].data.sort(sortByValue);
if (len == 0) {
continue;
}
var curMaxValue = dataInfos[x].data[len - 1].value;
var curMinValue = dataInfos[x].data[0].value;
if (!isSet.maxY || maxY < curMaxValue) {
maxY = curMaxValue;
isSet.maxY = true;
}
if (!isSet.minY || minY > curMinValue) {
minY = curMinValue;
isSet.minY = true;
}
dataInfos[x].data.sort(sortByKey);
var curMaxKey = dataInfos[x].data[len - 1].key;
var curMinKey = dataInfos[x].data[0].key;
if (!isSet.maxX || maxX < curMaxKey) {
maxX = curMaxKey;
isSet.maxX = true;
}
if (!isSet.minX || minX > curMinKey) {
minX = curMinKey;
isSet.minX = true;
}
}
if (maxY == minY) {
maxY += 20;
minY -= 20;
}
if (minX == maxX) {
maxX += 20;
minX -= 20;
}
date.setTime(maxX * 1000);
date.setHours(23,59,59);
maxX = parseInt(date.getTime() / 1000);
date.setTime(minX * 1000);
date.setHours(0,0,0);
minX = parseInt(date.getTime() / 1000);
dataInfo = dataInfos;
}
this.setMaxX = function(value){
maxX = value;
}
this.setMaxY = function(value){
maxY = value;
}
this.setMinX = function(value){
minX = value;
}
this.setMinY = function(value){
minY = value;
}
this.addMaxX = function(value){
maxX += value;
}
this.addMaxY = function(value){
maxY += value;
}
this.addMinX = function(value){
minX += value;
}
this.addMinY = function(value){
minY += value;
}
function canvsMouseMoveLister(event){
curMouseX = event.offsetX;
curMouseY = event.offsetY;
if (isInChart(curMouseX,curMouseY)) {
isShowDialog = true;
_self.draw();
} else if (isShowDialog){
isShowDialog = false;
_self.draw();
} else {
isShowDialog = false;
}
}
function canvsMouseLeave(){
isShowDialog = false;
_self.draw();
}
this.draw = function(){
if (!canvasid) {
return;
}
ctx.clearRect(0,0,c.width,c.height);
ctx.fillStyle = "#000000";
ctx.strokeStyle="#000";
//画左侧标题
ctx.font = "normal normal bold " + leftTitleFontSize + "px arial";
ctx.fillText(leftTitle,chartLeft / 6,chartTop - unitFontSize - leftTitleFontSize * 2);
//画最顶部标题
ctx.font = "normal normal normal " + leftTitleFontSize + "px arial";
ctx.fillText(title, (width - leftTitleFontSize * title.length) / 2, leftTitleFontSize * 1.5);
//画单位
ctx.font = "normal normal normal " + unitFontSize + "px arial";
ctx.fillText(unitstr,chartLeft - (unitFontSize * unitstr.length) / 2,chartTop - unitFontSize);
//画竖线
ctx.strokeStyle="#AAAAAA";
ctx.lineWidth = 0.5;
ctx.moveTo(chartLeft,chartTop);
ctx.lineTo(chartLeft,chartTop + heightChart);
ctx.stroke();
ctx.strokeStyle="#D1D1D1";
var lineHeight = heightChart / lineNunber;
//画横线
ctx.beginPath();
for (var i = 0;i <= lineNunber;i++) {
ctx.moveTo(chartLeft,chartTop + i * lineHeight);
ctx.lineTo(chartLeft + widthChart,chartTop + i * lineHeight);
}
ctx.stroke();
ctx.strokeStyle="#000000";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.font = "normal normal normal " + numberFontSize + "px arial";
//画最大最小值
date.setTime(maxX * 1000);
ctx.fillText(formatTime(date),chartLeft + widthChart,chartTop + heightChart + numberFontSize * 2);
date.setTime(minX * 1000);
ctx.fillText(formatTime(date),chartLeft,chartTop + heightChart + numberFontSize * 2);
//画左边标尺
for (var i = 0;i <= lineNunber;i++) {
var curvalue = (maxY - (i * lineHeight) / heightChart * (maxY - minY)).toFixed(0);
ctx.fillText(curvalue,chartLeft - (numberFontSize * curvalue.length) / 1.5 - lineShortWidth - 2,chartTop + i * lineHeight + numberFontSize / 3);
ctx.moveTo(chartLeft - lineShortWidth,chartTop + i * lineHeight);
ctx.lineTo(chartLeft,chartTop + i * lineHeight);
}
ctx.stroke();
//画 数据标题
ctx.font = "normal normal normal " + datatitleFontSize + "px arial";
//计算 总长度
var dataTitleLength = 0;
if (dataInfo.length > 0) {
for (let i in dataInfo) {
dataTitleLength += dataInfo[i].title.length * datatitleFontSize;
}
var datatitleStartPos = (width - (dataTitleLength + (dataTitleCirclePath + dataInfo.length * lineShortWidth) + (dataInfo.length - 1) * 5)) / 2;
var dataInfoHeight = leftTitleFontSize * 1.5 + datatitleFontSize * 1.5;
for (let i in dataInfo) {
var lineStartPos = datatitleStartPos + i * (dataTitleCirclePath + 2 * lineShortWidth + dataInfo[i].title.length * datatitleFontSize + 5);
var lineEndPos = lineStartPos + lineShortWidth;
var lineRightStartPos = lineEndPos + dataTitleCirclePath;
var lineRightEndPos = lineRightStartPos + lineShortWidth;
ctx.fillText(dataInfo[i].title,lineRightEndPos + 5,dataInfoHeight + datatitleFontSize / 3);
ctx.strokeStyle = dataInfo[i].color;
ctx.beginPath();
ctx.moveTo(lineStartPos,dataInfoHeight);
ctx.lineTo(lineEndPos,dataInfoHeight);
ctx.moveTo(lineRightStartPos,dataInfoHeight);
ctx.lineTo(lineRightEndPos,dataInfoHeight);
ctx.stroke();
ctx.beginPath();
ctx.arc(lineEndPos + dataTitleCirclePath / 2,dataInfoHeight,dataTitleCirclePath / 2,0,2*Math.PI);
ctx.stroke();
//画数据
var data = dataInfo[i].data;
var dataLen = data.length;
if (dataLen <= 0) {
continue;
}
ctx.beginPath();
for (let x in data) {
posX = valueXToPos(data[x].key);
posY = valueYToPos(data[x].value);
if (x == 0) {
ctx.moveTo(posX,posY);
} else if(x == dataLen - 1){
ctx.lineTo(posX,posY);
} else {
ctx.lineTo(posX,posY);
}
}
ctx.stroke();
ctx.beginPath();
var posX = valueXToPos(data[0].key);
var posY = valueYToPos(data[0].value);
ctx.arc(posX,posY,startLineCirclePath / 2,0,2*Math.PI);
ctx.stroke();
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
ctx.arc(posX,posY,startLineCirclePath / 2 - 1,0,2*Math.PI);
ctx.fill();
if (dataLen > 1) {
posX = valueXToPos(data[dataLen - 1].key);
posY = valueYToPos(data[dataLen - 1].value);
ctx.beginPath();
ctx.arc(posX,posY,endLineCirclePath / 2,0,2*Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.arc(posX,posY,endLineCirclePath / 2 - 1,0,2*Math.PI);
ctx.fill();
}
ctx.fillStyle = "#000000";
}
}
if (!isShowDialog) {
return;
}
//画当前坐标图
var dialogWidth = 80;
var dialogHeight = 30;
var dialogFontSize = 10;
//修正偏移值
//curMouseY -= 10;
//curMouseX -= 10;
var curValueY = posYToValue(curMouseY).toFixed(2) + "°";
var curValueX = posXToValue(curMouseX).toFixed(2);
date.setTime(curValueX * 1000);
var showStr = "X:"+ formatTime(date) + ",Y:" + curValueY;
dialogWidth = showStr.length * dialogFontSize * 0.7 + 10;
dialogHeight = dialogFontSize + 6;
ctx.fillStyle="#000000A0";
var dialogStartX = curMouseX - 10 - dialogWidth;
var dialogStartY = curMouseY - 10 - dialogHeight;
if (dialogStartX < chartLeft) {
dialogStartX = curMouseX + 10;
}
if (dialogStartX > chartLeft + widthChart) {
dialogStartX = curMouseX - 10 - dialogWidth;
}
if (dialogStartY < chartTop) { dialogStartY = curMouseY + 10; }
ctx.fillRect(dialogStartX,dialogStartY,dialogWidth,dialogHeight);
ctx.fillStyle = "#FFFFFF";
ctx.font = "normal normal normal " + dialogFontSize + "px arial";
ctx.fillText(showStr,dialogStartX + 5, dialogStartY + dialogFontSize);
ctx.strokeStyle="#FF0000";
ctx.beginPath();
ctx.moveTo(curMouseX,chartTop);
ctx.lineTo(curMouseX,chartTop + heightChart);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(chartLeft,curMouseY);
ctx.lineTo(chartLeft + widthChart,curMouseY);
ctx.stroke();
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}
function formatTime(date){
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
function formatTimeYMD(date){
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return [year, month, day].map(formatNumber).join('-')
}
function posXToValue(posx){
var valuex = ((posx - chartLeft) / widthChart) * (maxX - minX) + minX;
return valuex;
}
function valueXToPos(valuex){
var posx = ((valuex - minX) / (maxX - minX)) * widthChart + chartLeft;
return posx;
}
function posYToValue(posy){
var valuey = maxY - ((posy - chartTop) / heightChart) * (maxY - minY);
return valuey;
}
function valueYToPos(valuey){
var posy = heightChart - ((valuey - minY) / (maxY - minY)) * heightChart + chartTop;
return posy;
}
function isInChart(x,y){
return ((x > chartLeft - 10 && x < chartLeft + widthChart + 10)) && ((y > chartTop - 10 && y < chartTop + heightChart + 10));
}
}
function sortByKey(a,b){
return a.key - b.key;
}
function sortByValue(a,b){
return a.value - b.value;
}