使用canvas画折线图和曲线图

  1. 贝塞尔曲线如果想要在p0=》p2的过程中经过p1,那么需要计算出pc的值,在canvas之中作为控制点


    二次贝塞尔曲线转换为三次

    上面只是简单介绍,具体的参考文档在 这里

这里只把最终的结果贴出来
控制点A和B坐标计算公式

其中i是开始点,i+1是结束点,i-1是开始点的前一个点,i+2是结束点的后一个点,使用这个公式可以把中间部分的曲线画出来(不包含两头的那两段),其中的4可以取任意值(一般在3-10),两端的那一段因为一开始没有前一个点,所以这个i-1可以使用i这个点代替,后面那一段i+2可以使用i+1代替

下面是例子:使用react编写,可在自己的react项目里面跑一下看看:

底下的每一步都是在canvas里面单独画的,如果要性能优化可以处理一下,将相同的线条一次画完填色,不需要每次都重新开始begin
现在是点击canvas出现新的一幅图,如果想要动态变化,横坐标纵坐标慢慢移动变化,可以把canvas里面的变化点都存起来,在添加新坐标时记录出新的坐标在哪里,和保存的坐标进行对比,然后使用setInterval一步步的变化到新的坐标上实现(只是一种思路)

import React, { useEffect, useRef, useState } from 'react'
import ResizeObserver from 'resize-observer-polyfill'const CanvasLine = () => {const canvasRef = useRef<HTMLCanvasElement | null>()const [dataArr, setDataArr] = useState<number[]>([])const [modelData] = useState([20, 50, 70, 130, 60, 30, 90])const week = ['一', '二', '三', '四', '五', '六', '日']// 如果想做出动态数据展示,那么可以把所有的操作都保存下来,在最后统一渲染,在每一项需要移动的点(操作)上记录需要的操作,在渲染完成之后一步步的移动点,在到达最终位置,停止更新// const [showPoint, setShowPoint] = useState<any[]>()// 贝塞尔曲线const bse = (a: any, b: any, c: any, d: any) => {const canvasDom = canvasRef.current!const ctx = canvasDom.getContext('2d')!ctx.beginPath()ctx.moveTo(b.x, b.y)ctx.strokeStyle = 'red'const scale = 0.25 //分别对于ab控制点的一个正数,可以分别自行调整// 根据abcd四个点计算bc两个点的贝塞尔曲线// 具体的计算文档https://wenku.baidu.com/view/c790f8d46bec0975f565e211.html// 第一个控制点const pointOne: any = {}pointOne.x = b.x + (c.x - a.x) * scalepointOne.y = b.y + (c.y - a.y) * scaleconst pointTwo: any = {}pointTwo.x = c.x - (d.x - b.x) * scalepointTwo.y = c.y - (d.y - b.y) * scalectx.bezierCurveTo(pointOne.x, pointOne.y, pointTwo.x, pointTwo.y, c.x, c.y)ctx.stroke()ctx.closePath()}// 3.根据数据排序展示// 横坐标使用week,纵坐标自己计算数据确定范围// 监听元素宽度变化,根据宽度设置当前折线图的大小// space距离边的间隔  x:x轴最右端  y:y轴的最低端 xy的最小都是spaceconst checkData = (space: number, x: number, y: number) => {const dataLength = dataArr.lengthconst canvasDom = canvasRef.current!const ctx = canvasDom.getContext('2d')!// 获取数据最大值const maxNum = Math.max(...dataArr)// 获取数据最小值const minNum = Math.min(...dataArr)// 根据最大值最小值和space-x来设置// 计算x轴的坐标数据书写位置const xtemp = (x - space - 40) / dataLengthconst ytemp = (y - space - 40) / dataLength// 计算y轴坐标值,x轴的坐标值是直接取传进来的const yvalue = (maxNum - minNum) / dataLength// 开始渲染这些坐标x和获取x坐标const arrx = []for (let i = 0; i < dataLength; i++) {ctx.beginPath()ctx.font = '40px Microsoft YaHei'//水平对齐方式ctx.textAlign = 'center'//垂直对齐方式ctx.textBaseline = 'middle'ctx.fillText(week[i], space + i * xtemp, y + space / 2)ctx.stroke()arrx.push(space + i * xtemp)ctx.closePath()}// 开始渲染这些坐标yfor (let i = 1; i <= dataLength; i++) {ctx.beginPath()ctx.font = '40px Microsoft YaHei'//水平对齐方式ctx.textAlign = 'center'//垂直对齐方式ctx.textBaseline = 'middle'ctx.fillText((minNum + i * yvalue).toFixed(0), space / 2, y - ytemp * i)ctx.stroke()ctx.closePath()}// 通过获取到的x坐标来绘制点线for (let i = 0; i < dataLength; i++) {if (i != 0 && i != dataLength - 1 && i != dataLength - 2) {bse({ x: arrx[i - 1], y: y - (dataArr[i - 1] / maxNum) * (y - space - 40) },{ x: arrx[i], y: y - (dataArr[i] / maxNum) * (y - space - 40) },{ x: arrx[i + 1], y: y - (dataArr[i + 1] / maxNum) * (y - space - 40) },{ x: arrx[i + 2], y: y - (dataArr[i + 2] / maxNum) * (y - space - 40) })}// 这里是做几个情况的判断,在实际中是可以合并的,这里分开写容易理解if (i == 0 && dataLength > 2) {bse({ x: arrx[i], y: y - (dataArr[i] / maxNum) * (y - space - 40) },{ x: arrx[i], y: y - (dataArr[i] / maxNum) * (y - space - 40) },{ x: arrx[i + 1], y: y - (dataArr[i + 1] / maxNum) * (y - space - 40) },{ x: arrx[i + 2], y: y - (dataArr[i + 2] / maxNum) * (y - space - 40) })}if (i == dataLength - 2 && dataLength > 2) {bse({ x: arrx[i - 1], y: y - (dataArr[i - 1] / maxNum) * (y - space - 40) },{ x: arrx[i], y: y - (dataArr[i] / maxNum) * (y - space - 40) },{ x: arrx[i + 1], y: y - (dataArr[i + 1] / maxNum) * (y - space - 40) },{ x: arrx[i + 1], y: y - (dataArr[i + 1] / maxNum) * (y - space - 40) })}if (i == 1 && dataLength == 2) {bse({ x: arrx[i - 1], y: y - (dataArr[i - 1] / maxNum) * (y - space - 40) },{ x: arrx[i - 1], y: y - (dataArr[i - 1] / maxNum) * (y - space - 40) },{ x: arrx[i], y: y - (dataArr[i] / maxNum) * (y - space - 40) },{ x: arrx[i], y: y - (dataArr[i] / maxNum) * (y - space - 40) })}}ctx.beginPath()ctx.moveTo(space, y)for (let i = 0; i < dataLength; i++) {ctx.lineTo(arrx[i], y - (dataArr[i] / maxNum) * (y - space - 40))ctx.lineWidth = 2ctx.strokeStyle = '#000'ctx.stroke()ctx.strokeStyle = '#fff'if (i == dataLength - 1) {ctx.lineTo(arrx[i], y)}}const linearGradient = ctx.createLinearGradient(0, 0, 0, y)linearGradient.addColorStop(0, 'rgba(19,144,239,0.3)')linearGradient.addColorStop(0.8, 'rgba(255,255,255,0.3)')ctx.fillStyle = linearGradientctx.fill()ctx.closePath()}// 绘画折线图// 1.确定网格的大小,以决定可以画多少条x轴的线,多少y轴的线         画出网格const setGrid = () => {const canvasDom = canvasRef.current!const ctx = canvasDom.getContext('2d')!const gridSize = 50 //设置网格大小为10;const canvasHeight = canvasDom.heightconst canvasWidth = canvasDom.width// 重置画布的背景色ctx.fillStyle = '#fff'ctx.fillRect(0, 0, canvasWidth, canvasHeight)const xLineTotal = Math.floor(canvasHeight / gridSize)//画x轴条数for (let i = 0; i <= xLineTotal; i++) {ctx.beginPath()ctx.moveTo(0, i * gridSize - 0.5)ctx.lineTo(canvasWidth, i * gridSize - 0.5)ctx.strokeStyle = '#eee'ctx.stroke()}const yLineTotal = Math.floor(canvasWidth / gridSize)//画y轴条数for (let i = 0; i <= yLineTotal; i++) {ctx.beginPath()ctx.moveTo(i * gridSize - 0.5, 0)ctx.lineTo(i * gridSize - 0.5, canvasHeight)ctx.strokeStyle = '#eee'ctx.stroke()}}//2.绘制坐标系const setCoordinate = () => {//先确定原点:确定距离画布旁边的距离,通过距离算出原点const canvasDom = canvasRef.current!const ctx = canvasDom.getContext('2d')!//计算原点const canvasWidth = canvasDom.widthconst canvasHeight = canvasDom.height//确定坐标的长度://确定箭头的大小: 一半3,6,,直角边const space = 100const arrowSize = 12const lineWidth = 2const x0 = spaceconst y0 = canvasHeight - space//画x轴ctx.beginPath()ctx.moveTo(x0, y0 - 0.5)ctx.lineTo(canvasWidth - x0, y0 - 0.5)ctx.strokeStyle = '#000'ctx.lineWidth = lineWidthctx.stroke()ctx.closePath()//x轴画箭头ctx.beginPath()ctx.moveTo(canvasWidth - x0, y0)ctx.lineTo(canvasWidth - x0 - arrowSize, y0 + arrowSize / 2)ctx.lineTo(canvasWidth - x0 - arrowSize, y0 - arrowSize / 2)ctx.lineTo(canvasWidth - x0, y0)ctx.fillStyle = '#000'ctx.fill()//画y轴ctx.beginPath()ctx.moveTo(x0 - 0.5, y0)ctx.lineTo(x0 - 0.5, space)ctx.stroke()ctx.closePath()//画y轴箭头ctx.beginPath()ctx.moveTo(x0, space)ctx.lineTo(x0 + arrowSize / 2, space + arrowSize)ctx.lineTo(x0 - arrowSize / 2, space + arrowSize)ctx.lineTo(x0, space)ctx.fill()ctx.closePath()checkData(space, canvasWidth - x0, y0)}// 确定canvas大小const getOnSize = () => {const canvasDom = canvasRef.current!// 这里确定canvas大小是通过父元素大小来确定的,因为我这个项目最外层的父元素是这个,所以才取得这个元素,在实际中可做修改,或者写成需要的大小,这里是为了在元素宽高发生变化的时候canvas跟着改变const boxDom = canvasDom.closest('.ant-layout-content')!let boxDomWidth: numberlet boxDomHeight: numberconst fun = (data: any) => {if (canvasDom) {boxDomWidth = data[0].contentRect.widthboxDomHeight = data[0].contentRect.height// 获取到盒子的宽高之后就把画布的宽高设置成两倍canvasDom.height = boxDomHeight * 1canvasDom.width = boxDomWidth * 1// canvas元素的样式设置成一倍canvasDom.style.height = String(boxDomHeight + 'px')canvasDom.style.width = String(boxDomWidth + 'px')setGrid()setCoordinate()}}const observer = new ResizeObserver(fun)// 监听父元素大小变化,父元素大小变化就重新画canvasobserver.observe(boxDom)}useEffect(() => {getOnSize()}, [dataArr])const setDataFun = () => {setDataArr((a) => {if (dataArr.length == modelData.length) {return a}return [...a, modelData[dataArr.length]]})}return (<canvasonClick={() => {setDataFun()}}ref={(a) => {canvasRef.current = a}}></canvas>)
}
export default CanvasLine

结果:

值是[20,50,20,50,20,50,20]的图形

使用canvas画折线图和曲线图相关推荐

  1. 微信小程序canvas画价格走势图(一)

    今天是周二,今天来开一个新坑吧.最近花了整整两个工作日的时间画了一个微信小程序端的价格走势图,现在来分享一下经验. 因为内容比较多,所以打算这次分为多篇博客来写,这样一篇博客的内容就会更少.更清晰.下 ...

  2. python如何绘制曲线图_python pandas plot画折线图如何显示x轴的值?

    在使用python pandas Series plot画折线图时,不知道该如何显示x轴的值. 代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ...

  3. 如何用R画折线图,散点图,平滑曲线图

    如何用R画折线图,散点图,平滑曲线图 例子: week 1 2 3 4 5 6 x 3 8 19 24 6 1 y 1 25 21 3 2 1 要求是以week为横坐标,画出x-week,y-week ...

  4. python简单代码画曲线图教程-用Python画论文折线图、曲线图?几个代码模板轻松搞定!...

    前言 这几天在搞论文图,唉说实话抠图这种东西真能逼死人.坐在电脑前抠上一天越看越丑,最后把自己丑哭了-- 到了画折线图分析的时候,在想用哪些工具的时候.首先否决了excel,读书人的事,怎么能用exc ...

  5. python画折线图代码-用Python画论文折线图、曲线图?几个代码模板轻松搞定!

    前言 这几天在搞论文图,唉说实话抠图这种东西真能逼死人.坐在电脑前抠上一天越看越丑,最后把自己丑哭了-- 到了画折线图分析的时候,在想用哪些工具的时候.首先否决了excel,读书人的事,怎么能用exc ...

  6. matlab 折线图_用Python画论文折线图、曲线图?几个代码模板轻松搞定!

    前言 这几天在搞论文图,唉说实话抠图这种东西真能逼死人.坐在电脑前抠上一天越看越丑,最后把自己丑哭了-- 到了画折线图分析的时候,在想用哪些工具的时候.首先否决了excel,读书人的事,怎么能用exc ...

  7. 用Python画论文折线图、曲线图?几个代码模板轻松搞定!

    前言 这几天在搞论文图,唉说实话抠图这种东西真能逼死人.坐在电脑前抠上一天越看越丑,最后把自己丑哭了-- 到了画折线图分析的时候,在想用哪些工具的时候.首先否决了excel,读书人的事,怎么能用exc ...

  8. 计算机论文折线图,干货 | 画论文折线图、曲线图?几个代码模板轻松搞定!

    这几天在搞论文图,唉说实话抠图这种东西真能逼死人.坐在电脑前抠上一天越看越丑,最后把自己丑哭了-- 到了画折线图分析的时候,在想用哪些工具的时候.首先否决了excel,读书人的事,怎么能用excel画 ...

  9. python画折线图代码实现_用Python画论文折线图、曲线图?几个代码模板轻松搞定!...

    前言 这几天在搞论文图,唉说实话抠图这种东西真能逼死人.坐在电脑前抠上一天越看越丑,最后把自己丑哭了-- 到了画折线图分析的时候,在想用哪些工具的时候.首先否决了excel,读书人的事,怎么能用exc ...

最新文章

  1. 跨学科整合,打造大数据最强集团军:清华大学大数据能力提升项目宣讲会来了!...
  2. 全新的Spring Authorization Server快速入门
  3. 监听ListView滚动到最底部
  4. hbase java 端口_HBase远程Java客户端尝试通过随机端口进行身份验证
  5. 产品如何解决「发型师」与「消费者」的认知偏差?
  6. 推荐一个python学习的宝库(github的star数71000+)
  7. SAP Spartacus pageSlot一览
  8. elementui表格中tip设置_VUE2.0+ElementUI2.0表格el-table:表头扩展el-tooltip
  9. python学习笔记(二十)初识面向对象
  10. mysql中教如何拼接字段(列)值、加入运算、设置别名(非常实用)
  11. 美团王兴:互联网下半场基本功不过关 活下去都很难
  12. python server酱_用Python抢到回家的车票,so easy
  13. python 将字符串转换为字典
  14. 安卓 java hook 免root_[原创]利用VirtualApp实现免Root注入Hook(一)
  15. 一文掌握大数据架构师需要具备的能力和格局
  16. kubernetes笔记
  17. python 基于numpy的线性代数运算
  18. Swift 使用NSRange 查找字符多次出现的位置处理
  19. 启用Windows沙盒
  20. SLF4J及其MDC详解

热门文章

  1. 万字长文,冲刺备战金九银十,奉上[Java一线大厂高岗面试题解析合集]
  2. 【学习笔记】OFDM中信道估计技术分析与实现
  3. 如何运用VR3d模型线上展示构建博物馆展厅与展馆
  4. IntelliJ IDEA 中如何导入jar包(以引入spring包,构建spring测试环境为例)
  5. 地使用分类数据下载介绍
  6. [转]家庭上网安装与设置教程
  7. caffe 搭建参数服务器(1)—— 用MPI实现多节点同时训练一个模型
  8. 查询Apple app的bundle ID
  9. 云计算已渗透大众生活,去中心化云计算发展前景广阔
  10. bms中soh计算方式_电动汽车BMS中SOH和SOP估算策略总结