开发案例:使用canvas实现图表系列之折线图
一、功能结构
实现一个公共组件的时候,首先分析一下大概的实现结构以及开发思路,方便我们少走弯路,也可以使组件更加容易拓展,维护性更强。然后我会把功能逐个拆开来讲,这样大家才能学习到更详细的内容。下面简单阐述下折线图组件的功能结构:
以上是基础的功能结构框架,包含一些比较简单的基础功能,后续还有点击触发、动画等功能也会规划进去。这一期我们先实现上面这些基础的功能,后续再慢慢拓展。
二、公共属性
1. 一个组件肯定会有一些公共的属性作为动态参数,便于组件之间的信息传递,我们分别讲解一下五个公共属性的作用:画布的宽度(cWidth)和高度(cHeight),这个是最基本的。但是我这里控制是非必传,默认值都是100%就可以了。
2. 画布的内部留白间距(cSpace)。主要是用来控制内容区与画布外框的距离,避免绘画的内容被截掉。
3. 字体大小(fontSize)。主要是来控制整个绘画内容的字体大小,全局性,避免每个小功能都需要传字体大小。
4. 字体颜色(color)。与字体大小的功能一致。
5. 图表数据(data)。用来存储图表内容的数组,其中name与value是必传的。
以下是具体的代码:
// 图表数据的特征接口
interface interface_data {
name: string | number;
value: string | number;
[key: string]: any;
} // 图表的特征接口
interface interface_option {
cWidth?: string | number,
cHeight?: string | number,
fontSize?: string | number,
color?: string,
cSpace?: number,
data?: interface_data[]
} // option 默认值
const def_option: interface_option = {
cWidth: '100%',
cHeight: '100%',
fontSize: 10,
color: '#333',
cSpace: 20,
data: []
} @Component
export struct McLineChart {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
@State options: interface_option = {}
aboutToAppear() {
this.options = Object.assign({}, def_option, this.options)
}
build() {
Canvas(this.context)
.width(this.options.cWidth)
.height(this.options.cHeight)
.onReady(() => { })
}
}
三、绘画坐标轴
绘画图表内容区部分,首先是绘画坐标轴,坐标轴分为X轴跟Y轴,我们要先开始画Y轴,原因是:y轴上要显示文本标签,如果一开始没有得到文本标签对应的宽度最大值,那么Y轴跟X轴的起点坐标就会有偏差,会导致绘画全部错位,下图是完整的坐标轴的效果。
1.绘画Y轴
Y轴整体是由轴线、分割线、刻度线、文本标签四个部分组成的,四个部分都有先后关系,而且包含一定的算法逻辑,下面简单用一个概念图进行讲解。
首先用500*500的矩形作为我们这次的画布,我们可以在图上看到Y轴整体包含了文本标签、Y轴线、分割线、刻度线四个部分。而canvas绘画基本都是通过坐标来定位的,Y轴整体的四个部分的起点与结束坐标都互相有关系,甚至需要把内部间距、分割间距、y轴线高度、文本最大的宽度四个属性计算在内。以上是概念与思路,接下来我们逐一讲解代码:
1、计算得到文本最长宽度(maxNameW ),我们可以从图中看到,不论是y轴线、刻度线还是分割线的起点坐标都是需要内容间距、文本标签、文本标签与分割线间隔相加计算得到,而为了保持对齐,所以我们需要计算出文本最长宽度。而y轴的文本一般都是数据(data)对应的数值,所以我们需要得到传入数据(data)中的最大值。然后讲最大值分割成五等分。以下就是计算获取最大文本宽度的代码,部分逻辑我也会写在代码上:
build() {
Canvas(this.context)
.width(this.options.cWidth)
.height(this.options.cHeight)
.backgroundColor(this.options.backgroundColor)
.onReady(() => {
const values: number[] = this.options.data.map((item) => Number(item.value || 0))
const maxValue = Math.max(...values)
let maxNameW = 0
let cSpiltNum = 5 // 分割等分
let cSpiltVal = maxValue / cSpiltNum // 计算分割间距
for(var i = 0; i <= this.options.data.length; i++){
// 用最大值除于分割等分得到每一个文本的间隔值,而每一次遍历用间隔值乘于i就能得到每个刻度对应的数值了,计算得到得知需要保留整数且转成字符串
const text = (cSpiltVal * i).toFixed(0)
const textWidth = this.context.measureText(text).width; // 获取文字的长度
maxNameW = textWidth > maxNameW ? textWidth : maxNameW // 每次进行最大值的匹配
}
})
}
2、绘画文本标签,我们可以从图中看到文本标签的x坐标只跟内部间距有关,而且我们从上面代码就已经得到每个刻度的分割间距了,从而可以得到每个文本的y轴。
.onReady(() => {
....
for(var i = 0; i <= this.options.data.length; i++){
...
// 绘画文本标签
this.context.fillText(text, this.options.cSpace, cSpiltVal * (this.options.data.length - i) + this.options.cSpace , 0);
}
})
3、绘画刻度线。我们可以从概念图得到,刻度线的起点x坐标算法是:内部间距(cSpace)加最长文本宽度(maxNameW )加上文本与刻度线的间距,起点y坐标则跟文本一样,通过分割间距与下角标的关系得到每个刻度的y坐标;而终点x坐标则是刻度线的长度,终点y坐标则跟起点的y坐标一样,我设置默认长度是5,这样就能得到我们的刻度线了。代码如下:
.onReady(() => {
....
const length = this.options.data.length
for(var i = 0; i <= length; i++){
...
}
// 上面是获取最长文本宽度的代码
// 画线的方法
function drawLine(x, y, X, Y){
this.context.beginPath();
this.context.moveTo(x, y);
this.context.lineTo(X, Y);
this.context.stroke();
this.context.closePath();
}
for(var i = 0; i <= length; i++){
const item = this.options.data[i]
// 绘画文本标签
ctx.fillText(text, this.options.cSpace, cSpiltVal * (this.data.length - i) + this.options.cSpace, 0);
// 内部间距+文本长度
const scaleX = this.options.cSpace + maxNameW
// 通过数据最大值算出等分间隔,从而计算出每一个的终点坐标
const scaleY = cSpiltVal * (length - i) + this.options.cSpace
// 这里的5就是我设置文本跟刻度线的间隔与刻度线的长度
drawLine(scaleX, scaleY, scaleX + 5 + 5, scaleY);
}
})
4、绘画y轴线。继续分析概览图,从图中我们可以得到:y轴线的起点x坐标的算法是:内部间距(cSpace)加最长文本宽度(maxNameW )加上文本与刻度线的间距以及刻度线长度,起点y坐标则是内部上间距;而终点x坐标与起点x坐标相同,终点y坐标算法是:画布高度减去上下两边的内部间距。通过以上计算关系就能绘画出y轴线了。代码如下:
.onReady(() => {
...
// 上面是绘画其他组成部分代码
const startX = this.options.cSpace + maxNameW + 5 + 5
const startY = this.options.cSpace
const endX = startX
const endY = this.context.height - (this.options.cSpace * 2)
drawLine(startX, startY, endX, endY); // 绘画y轴
})
5、绘画分割线。其实从图中可以看出分割线与刻度线差不多,起点x坐标算法是:在刻度线起点x坐标基础上加刻度线长度;起点y轴与刻度线相同。而终点的x坐标算法:画布宽度减去起点x坐标;终点的y坐标与起点的y坐标相同。具体代码如下:
.onReady(() => {
....
// 上面是获取最长文本宽度的代码
for(var i = 0; i <= length; i++){
const item = this.options.data[i]
// 绘画文本标签跟刻度
...
// 绘画分割线
const splitX = scaleX + 5 + 5
const splitY = scaleY
drawLine(splitX, splitY, this.context.width - splitX - this.options.cSpace, splitY);
}
})
2.绘画X轴
绘画完Y轴之后,我们接着绘画X轴, X轴与Y轴绘画逻辑一致,只是方向不同而已。具体的算法就不一一详解,可以参考一下概念图。
而与绘画Y轴不一致的在于:
1. 最长对象不一样。Y轴最长是文本宽度;而X轴需要获取的最长是文本高度。
2. 间隔分割数不一样。Y轴是自定义的分割数;而X轴分割线是实际数据的长度。
3. 分割间距长度算法不一样。Y轴算法是用数据最大值处于自定义的分割数;而X轴算法是用画布宽度减去(左右两边的内部间隙以及Y轴宽度(文本最长宽度加上刻度线宽度)),再除去数据的长度,得到每个间隔的长度。
除了上面三点需要注意的,其他的就是调换一下计算的位置。X轴整体的代码如下:
.onReady(() => {
const cSpace = this.options.cSpace
// 上面是绘制y轴的代码
....
// 绘制x轴
// 获取每个分割线的间距:this.context.width - 20为x轴的长度
let xSplitSpacing = parseInt(String((this.context.width - cSpace * 2 - maxNameW) / this.options.data.length))
let x = 0;
for(var i = 0; i <= this.options.data.length; i++){
// 绘画分割线
x = xSplitSpacing * (i + 1) // 计算每个数值的x坐标值
this.drawLine(x + cSpace + maxNameW, this.context.height - cSpace, x + cSpace + maxNameW, cSpace);
// 绘制刻度
this.drawLine(x + cSpace + maxNameW, this.context.height - cSpace, x + cSpace + maxNameW, this.context.height - cSpace);
// 绘制文字刻度标签
const text = this.options.data[i].name
const textWidth = this.context.measureText(text).width; // 获取文字的长度
// 这里文本的x坐标需要减去本身文本宽度的一半,这样才能居中显示, y坐标这是画布高度减去内部间距即可
this.context.fillText(text, x + cSpace + maxNameW - textWidth / 2, this.context.height - cSpace, 0);
}
this.context.save();
this.context.rotate(-Math.PI/2);
this.context.restore();
})
四、绘画折线区
绘画完坐标轴之后,就可以来绘画折线区的内容了。也是整个画布重点的部分。折线区分为三个部分:绘画折线、绘画标点、绘画文本。1.绘画折线
从上面的图可以看出折线直接就是把实际数据的数值转成x跟y坐标,再通过连线连接起来。而每一个转折点的x坐标算法跟x轴的刻度或者文本是一样的,而y坐标是实际数值通过一定算法转成我们需要的高度。x坐标我们已经获取了,只要是攻克我们的y坐标即可。可以通过图来观察一下在画布中与实际数据的关系:
首先Y轴的高度代表的是实际数据的最大值,这个我们绘画Y轴的时候就得到的结果,那我们则可以算出Y轴高度与实际数据的缩放倍数(scale),而折线的的每个y坐标对应的也是实际数值,需要把实际数值转换成画布中高度,那么就用实际数值乘与刚刚得到的缩放倍数(scale)就能得到转化后的高度了。
虽然我们已经得到每个转折点缩放后的高度,但是如果要跟Y轴坐标一一对应的y坐标的画,还需要用画布的高度减去下边内部高度加x轴高度,再减去缩放后的实际高度。这样算出来的才是我们想要的y坐标值,大概算法关系已经知道了,以下是最终代码:
.onReady(() => {
...
// 上面是绘制x轴跟y轴的代码
// 绘画折线
const ySacle = (this.context.height - cSpace *2) / maxValue // 计算出y轴与实际最大值的缩放倍数
//连线
this.context.beginPath();
for(var i=0; i< this.options.data.length; i++){
const dotVal = String(this.options.data[i].value);
const x = xSplitSpacing * (i + 1) + cSpace + maxNameW // 计算每个数值的x坐标值
const y = this.context.height - cSpace - parseInt(dotVal * ySacle); // 画布的高度减去下边内部高度加x轴高度,再减去缩放后的实际高度
if(i==0){
// 第一个作为起点
this.context.moveTo( x, y );
}else{
this.context.lineTo( x, y );
}
}
ctx.stroke();
})
2.绘画标点、文本标签
画完折线我们基本能得到很多东西,比如折线上每个转折点的x跟y坐标值。这样对我们绘画标点与文本标签就很方便了:
.onReady(() => {
...
// 上面是绘制x轴跟y轴的代码
// 绘画折线
const ySacle = (this.context.height - cSpace *2) / maxValue // 计算出y轴与实际最大值的缩放倍数
this.context.beginPath();
for(var i=0; i< this.options.data.length; i++){
// 绘画折线代码
...
// 绘制标点
drawArc(x, y);
// 绘制文本标签
const textWidth = this.context.measureText(dotVal).width; // 获取文字的长度
const textHeight = this.context.measureText(dotVal).height; // 获取文字的长度
this.context.fillText(dotVal, x - textWidth / 2, y - textHeight / 2); // 文字
} function drawArc( x, y ){
this.context.beginPath();
this.context.arc( x, y, 3, 0, Math.PI*2 );
this.context.fill();
this.context.closePath();
}
this.context.stroke();
})
最终效果如下:
五、总结
以上是本次技术分析,希望能对大家有所启发,也祝愿各位开发者能开发出理想的效果,后续我们会把chart相关系列的组件封装到组件库发布到市场上,这样可以直接开箱即用了。敬请期待吧,后续还有很多技术的分享,不要错过!
开发案例:使用canvas实现图表系列之折线图的更多相关文章
- Wijmo金融图表系列之等量图&成交量柱状图
Wijmo金融图表有很多类型,我们来一一介绍.之前介绍了平均K线图(Heikin-Ashi)和砖形图,现在我们来一起看看等量图和成交量柱状图. 图表 #3: 等量图(EquiVolume) 等量图和K ...
- 【转】使用DevExpress的WebChartControl控件绘制图表(柱状图、折线图、饼图)
第一次写博,没什么经验,主要是把最近自己对Dev的一些研究贴出来大家共同探讨,有不足之处望大家帮忙斧正. WebChartControl是DevExpress控件群下的一个Web图表控件,它使用非常的 ...
- Android开发学习之路-自定义控件(天气趋势折线图)
之前写了个天气APP,带4天预报和5天历史信息.所以想着要不要加一个折线图来显示一下天气变化趋势,难得有空,就写了一下,这里做些记录,脑袋不好使容易忘事. 先放一下效果: 控件内容比较简单,就是一个普 ...
- 准备开发一个基于canvas的图表库,记录一些东西(一)
开源的图表库已经有很多了,这里从头写个自己的,主要还是 提高自己js的水平,增加复杂代码组织的经验 首先写一个画图的库,供以后画图表使用.经过2天的开发,算是能拿出点东西了,虽然功能还很弱,但是有了一 ...
- 使用Canvas和Paint自己绘制折线图
主要用于Canvas一个特别简单的小demo. 能够手动点击看每一个月份的数据.很easy.就是用paint在canvas上画出来的. 主要内容就是计算左边价格的位置,以下日期的位置,三根虚线的位置, ...
- ajax实现highchart与数据库数据结合完整案例分析(三)---柱状折线图
作者原创,未经博主允许,不可转载 在前面分析和讲解了用java代码分别实现饼状图和折线图,在工作当中,也会遇到很多用ajax进行异步请求 实现highchart. 先展示一下实现的效果图: 用ajax ...
- Wijmo金融图表系列之平均K线图&砖形图
2015年7月16日将会发布有史以来最令人兴奋的控件-Wijmo 金融图表,它的一体化设计为单个自定义集合提供了所有主要的金融图表,这是市场上的其他控件都不具备的独一无二的好处.它像Wijmo其他任意 ...
- 利用JFreeChart生成折线图 (4) (转自 JSP开发技术大全)
利用JFreeChart生成折线图 (4) (转自 JSP开发技术大全) 14.4 利用JFreeChart生成折线图 通过JFreeChart插件,既可以生成普通效果的折线图,也可以生成3D效果的折 ...
- 【Canvas】(2)---绘制折线图
绘制折线图 之前在工作的时候,用过百度的ECharts绘制折线图,上手很简单,这里通过canvas绘制一个简单的折线图.这里将一整个绘制过程分为几个步骤: 1.绘制网格 2.绘制坐标系 3.绘制点 4 ...
- canvas图表详解系列(2):折线图
本章建议学习时间4小时 学习方式:详细阅读,并手动实现相关代码(如果没有canvas基础,需要先学习前面的canvas基础笔记) 学习目标:此教程将教会大家如何使用canvas绘制各种图表,详细分解步 ...
随机推荐
- 【Azure 媒体服务】记录一个简单的媒体视频上传到Media Service无法播放问题
问题描述 从本地上传到Azure Media Service Portal的视频,并且新增定位符后,无法播放.但是上传的其他视频是可以的.疑惑中!! 问题自查 自查发现,是视频的文件名中有个特殊符号. ...
- 【Azure 事件中心】如何查看事件中心的消息中具体报文内容呢?
问题描述 如何查看事件中心的消息中具体报文内容呢? 问题解答 正常情况是通过 Event Hub 的消费端获取消息进行处理查看,但是没有客户端代码的情况下,也可以通过微软的默认客户端Service B ...
- Spark任务性能调优总结
一.shuffle调优 大多数Spark作业的性能主要就是消耗在了shuffle环节,因为该环节包含了大量的磁盘IO.序列化.网络数据传输等操作.因此,如果要让作业的性能更上一层楼,就有必要对shuf ...
- C++11的类型转换
//C类型转换 /* C语言:显式和隐式类型转换 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败 显式类型转化:需要用户自己处理. 隐式类型:界定:相关类型,相近类型,意义相似的类 ...
- ANDROID : NEW IDEA
•前言 学习了 Android 开发后,小脑袋瓜中时不时会蹦跶出一些想法: 这些想法大都是我现在实现不了的,所以就需要记录一下,等学到相关知识时,在着手解决. •NEW IDEA 1 将一款 APP( ...
- Vue mixin 永远不要用!! 极品垃圾,后期维护就剩骂街了~!!
为什么 Vue mixin 永远不要用!! 极品垃圾,后期维护就剩骂街了~!! vscode 没有自动跳转 自己维护下看看,重构的时候,还得整个复写,相当于整个软件重写 F!!!
- Set-Alias navi ./navi.bat - 设置别名 - powershell入门 (后期改方案了,换npm script)
需求 开机要启动好几个服务,原先都用vscode启动,觉得可能比较占内存,所以改成命令行 发现直接运行bat,需要输入./batName.bat 一次还行,天天输入就麻烦了 命令 Set-Alias ...
- vim没有clipboard,没法复制到系统剪切板,通过xclip将复制、删除的内容放到系统剪切板
解决方法:在/etc/vim/vimrc 或者 ~/.vimrc 中添加下面的命令 au TextYankPost * exe system("xclip -selection clipbo ...
- 基于stm32H730的解决方案开发之freertos系统解析
一 概述 在嵌入式小系统领域,freertos是一个非常厉害的角色.它和小芯片结合,能迸发出非常大的威力.这里在H730上使用了这个freertos,是应该做一个总结和备忘. 二 实例解析 1 线程初 ...
- 35_音视频播放器_seek&暂停
目录 一.实现seek功能 二.解决点击seek操作时会出现画面快速闪过 三.实现暂停功能 3.1.音频暂停 3.2.视频暂停 一.实现seek功能 我们主要是使用ffmpeg的av_seek_fra ...