前言
本章节,我们将实现图表中的折线图。
首先我们先确认一下我们需要绘制图形的数据格式:
[
{ item: '2000', a: 60, b: 153 },
{ item: '2001', a: 67, b: 134 },
{ item: '2002', a: 64, b: 133 },
{ item: '2003', a: 78, b: 148 },
{ item: '2004', a: 65, b: 165 },
{ item: '2005', a: 66, b: 188 },
{ item: '2006', a: 70, b: 145 },
{ item: '2007', a: 80, b: 173 },
{ item: '2008', a: 88, b: 177 },
{ item: '2009', a: 90, b: 168 },
{ item: '2010', a: 76, b: 187 },
{ item: '2011', a: 77, b: 196 },
{ item: '2012', a: 89, b: 200 },
{ item: '2013', a: 65, b: 173 },
{ item: '2014', a: 79, b: 190 },
]
item为X轴坐标值,其余属性为各折线的属性值。
基本的折线图
基础的折线图我们可以简单的看作多个线图与坐标层组合,效果图如下:
- 遍历数据确定数据的最大值及最小值;
- 根据数据最大值与最小值,计算单位数值所占用的长度;
- 遍历数据,计算各个数据的坐标点;
- 根据坐标点,绘制MultiLine对象,并加入线图层;
代码如下:
make() {
this.childs.splice(0, this.childs.length);
if(this.data.length === 0) {
return;
}
let max = -99999999; // 数据最大值
let min = 99999999; // 数据最小值
//遍历数据,确定最大值与最小值
for (let i = 0; i < this.data.length; i++) {
for (let key in this.data[i]) {
if (key !== 'item') {
if (max < this.data[i][key]) {
max = this.data[i][key];
}
if (min > this.data[i][key]) {
min = this.data[i][key];
}
}
}
}
// 计算Y方向单位数值所占用的长度
const yStep = this.height / (max - min);
// 计算X方向单位数值所占用的长度
const xStep = this.width / this.data.length;
// 记录各线条所在的点
const linePoints = {};
// 遍历数据计算点的位置
for (let i = 0; i < this.data.length; i++) {
const data = this.data[i];
for(let key in data) {
if (key !== 'item') {
if (!linePoints[key]) {
linePoints[key] = [];
}
linePoints[key].push(
new Point(
this.position.x + i * xStep,
this.position.y + (data[key] - min) * yStep
)
);
}
}
}
// 遍历所有的点数据,绘制相应曲线
for (let key in linePoints) {
let line = new MultiLine(this.canvas, {
...this.style,
color: this.colors[key],
position: linePoints[key][0],
}, linePoints[key]);
this.addChild(line);
}
this.onMaked && this.onMaked(this, {
xStep,
yStep,
yMax: max,
yMin: min,
});
}
面积图
// 遍历所有的点数据,绘制相应曲线
for (let key in linePoints) {
let line = new MultiLine(this.canvas, {
...this.style,
color: this.colors[key],
position: linePoints[key][0],
}, linePoints[key]);
// 绘制面积图
if (this.type === LineLayer.TYPE.FILL) {
let polygon = new Polygon(this.canvas, {
color: this.colors[key],
type: Polygon.TYPE.FILL,
}, [
new Point(linePoints[key][0].x, 20),
...linePoints[key],
new Point(linePoints[key][linePoints[key].length - 1].x, 20)
]) // 增加前后两个点使其闭合
polygon.alpha = 0.4; // 修改透明度
this.addChild(polygon);
}
this.addChild(line);
}
当面积图穿过0轴时,一个线条可能需要多个多边形,我们需要采用两点间差值来计算多边形,这里便不展开讨论,有兴趣的同学可以自己实现。
拖动与缩放
对于大数据量,一屏无法展示所有数据,所以我们需要对画布进行拖动与缩放。
画布的拖动我们可以转换思路为所要绘制的数据索引的变动。假定当前屏幕所绘制的数据索引为300到400的数据,当我们屏幕向左拖动100个数据单位长度时,我们需要绘制200-300的数据。
画布的缩放我们可以转变为当前屏幕需要绘制的数据大小。假定当前屏幕绘制数据100个,当我们放大屏幕时,我们所需要绘制的数据将减少,例如放大1倍,我们只需要绘制50个数据。
修改基本折线图层的代码,修改如下:
make() {
this.childs.splice(0, this.childs.length);
if(this.data.length === 0) {
return;
}
// 计算X方向单位数值所占用的长度
const xStep = this.width / this.showNum;
this.xStep = xStep;
// 数据偏移量
let kLeft = Math.floor(this.position.x / xStep);
// 数据的结束索引
let end = this.data.length;
let start = this.data.length - this.showNum;
if (kLeft > 0 && this.data.length > kLeft + this.showNum) {
// 向右移动数量与显示数量小于数据量, 结束索引等于数据量-偏移量
end = this.data.length - kLeft;
start = end - this.showNum;
} else if (kLeft > 0) {
// 向右移动数量超过数据量, 结束索引等于显示数量
end = this.showNum;
start = 0;
} else {
// 向左移动
end = this.data.length - 1;
start = end - this.showNum - kLeft;
}
this.start = start;
this.end = end;
let max = -99999999; // 数据最大值
let min = 99999999; // 数据最小值
//遍历数据,确定最大值与最小值
for (let i = start; i < end; i++) {
for (let key in this.data[i]) {
if (key !== 'item') {
if (max < this.data[i][key]) {
max = this.data[i][key];
}
if (min > this.data[i][key]) {
min = this.data[i][key];
}
}
}
}
// 计算Y方向单位数值所占用的长度
const yStep = this.height / (max - min);
// 记录各线条所在的点
const linePoints = {};
// 遍历数据计算点的位置
for (let i = start; i < end; i++) {
const data = this.data[i];
for(let key in data) {
if (key !== 'item') {
if (!linePoints[key]) {
linePoints[key] = [];
}
linePoints[key].push(
new Point(
this.shiftLeft + (i - start) * xStep,
this.position.y + (data[key] - min) * yStep
)
);
}
}
}
// 遍历所有的点数据,绘制相应曲线
for (let key in linePoints) {
let line = new MultiLine(this.canvas, {
...this.style,
color: this.colors[key],
position: linePoints[key][0],
}, linePoints[key]);
// 绘制面积图
if (this.type === LineLayer.TYPE.FILL) {
let polygon = new Polygon(this.canvas, {
color: this.colors[key],
type: Polygon.TYPE.FILL,
}, [
new Point(linePoints[key][0].x, 20),
...linePoints[key],
new Point(linePoints[key][linePoints[key].length - 1].x, 20)
]) // 增加前后两个点使其闭合
polygon.alpha = 0.4; // 修改透明度
this.addChild(polygon);
}
this.addChild(line);
}
this.onMaked && this.onMaked(this, {
start,
end,
xStep,
yStep,
yMax: max,
yMin: min,
});
}
除了修改折线图层的绘制函数之外,我们还需要给线图层添加拖放事件与缩放事件,代码如下:
// 监听拖动事件
this.line.addEventListener(Event.EVENT_DRAG, (e) => {
this.onChartDrag(e);
});
// 拖动结束事件
this.line.addEventListener(Event.EVENT_DRAG_END, (e) => {
this.onChartDragEnd(e);
});
// 监听滚轮缩放
this.line.addEventListener(Event.EVENT_WHEEL, (e) => {
this.onChartScale(e);
});
由上面代码可以看出,我们还添加了拖动结束事件监听,该监听用于拖动结束后的惯性效果,使得拖动更加自然。
拖动代码如下:
/**
* 图表缩放
* @param e
*/
onChartScale = (e) => {
if(this.line.locked) {
return;
}
if (e.nativeEvent.wheelDeltaY < 0) {
if (this.line.showNum < this.line.data.length) {
this.line.showNum = Math.round(this.line.showNum * 1.1);
this.line.make();
this.canvas.paint();
}
} else {
if (this.line.showNum > 20) {
this.line.showNum = Math.round(this.line.showNum * 0.9);
this.line.make();
this.canvas.paint();
}
}
}
onChartDrag = (e) => {
if (this.line.locked) {
return;
}
if (
this.line.position.x + e.distanceX
>= (this.line.data.length - this.line.showNum) * this.line.barWidth
) {
// 移动的距离超过所有数据的长度, 设置为最大长度
this.line.setPosition((this.line.data.length - this.line.showNum) * this.line.barWidth, this.line.position.y);
} else if(this.line.position.x <= - this.line.width / 2) {
// 至少保证K线数据占据半屏
this.line.setPosition(-this.line.width / 2, this.line.position.y);
}else {
// 线平移相应的距离
this.line.setPosition(this.line.position.x + e.distanceX, this.line.position.y);
}
this.line.make();
this.canvas.paint();
}
// 移动结束的关心效果
onChartDragEnd = (e) => {
if (this.barLayer.locked) {
return;
}
const accelerate = e.speedX > 0 ? 3000 : -3000; // 加速度3000画布像素每秒
const duration = Math.abs(e.speedX / accelerate);
this.accelerateAction = new AccelerateAction(duration, {
speedX: e.speedX,
accelerateX: accelerate,
beforeUpdate: (node, frame) => {
if (node.position.x >= (this.barLayer.data.length - this.barLayer.showNum) * node.barWidth) {
node.setPosition((this.barLayer.data.length - this.barLayer.showNum) * node.barWidth, node.position.y);
} else if(node.position.x <= - this.barLayer.width / 2) {
node.setPosition(- this.barLayer.width / 2, node.position.y);
}
node.make();
}
});
this.barLayer.runAction(this.accelerateAction, (node, action) => {
// 保证不移出画布
if (node.position.x >= (this.barLayer.data.length - this.barLayer.showNum) * node.barWidth) {
node.stopAction(action);
node.setPosition((this.barLayer.data.length - this.barLayer.showNum) * node.barWidth, node.position.y);
} else if (node.position.x <= - this.barLayer.width / 2) {
node.stopAction(action);
node.setPosition(- this.barLayer.width / 2, node.position.y);
}
});
}
拖动结束后我们使线图进行了一个加速度动作AccelerationAction,其源代码如下:
export default class AccelerateAction extends Action {
constructor(duration, option) {
super(duration, 10);
this.speedX = option.speedX || 0;
this.speedY = option.speedY || 0;
this.accelerateX = option.accelerateX || 0;
this.accelerateY = option.accelerateY || 0;
this.beforeUpdate = option.beforeUpdate;
}
/**
* 更新函数
* @param node
* @param frame
*/
update(node, frame) {
if (this.allFrames === 0 && this.status !== Action.STATUS.RUNNING) {
return;
}
const t = 1 / this.fps;
// 计算X方向与Y方向的速度
const vX = this.speedX + frame / this.fps * this.accelerateX / 1000;
const vY = this.speedY + frame / this.fps * this.accelerateY / 1000;
// 根据加速度公式计算移动距离
const distX = vX * t + 0.5 * this.accelerateX * t * t;
const distY = vY * t + 0.5 * this.accelerateY * t * t;
if (!node.locked) {
// 移动图层位置
node.setPosition(node.position.x + distX, node.position.y + distY);
this.beforeUpdate && this.beforeUpdate(node, frame);
node.canvas.paint();
}
}
}
目录
【实现自己的可视化引擎01】认识Canvas
【实现自己的可视化框架引擎02】抽象图像元素
【实现自己的可视化引擎03】构建基础图元库
【实现自己的可视化引擎04】图像元素动画
【实现自己的可视化引擎05】交互与事件
【实现自己的可视化引擎06】折线图
【实现自己的可视化引擎07】柱状图
【实现自己的可视化引擎08】条形图
【实现自己的可视化引擎09】饼图
【实现自己的可视化引擎10】散点图
【实现自己的可视化引擎11】雷达图
【实现自己的可视化引擎12】K线图
【实现自己的可视化引擎13】仪表盘
【实现自己的可视化引擎14】地图
【实现自己的可视化引擎15】关系图