使用canvas画折线图和曲线图

使用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) * scale
    pointOne.y = b.y + (c.y - a.y) * scale
    const pointTwo: any = {}
    pointTwo.x = c.x - (d.x - b.x) * scale
    pointTwo.y = c.y - (d.y - b.y) * scale
    ctx.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的最小都是space
  const checkData = (space: number, x: number, y: number) => {
    const dataLength = dataArr.length
    const 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) / dataLength
    const 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()
    }
    // 开始渲染这些坐标y
    for (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 = 2
      ctx.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 = linearGradient
    ctx.fill()
    ctx.closePath()
  }
  // 绘画折线图
  // 1.确定网格的大小,以决定可以画多少条x轴的线,多少y轴的线         画出网格
  const setGrid = () => {
    const canvasDom = canvasRef.current!
    const ctx = canvasDom.getContext('2d')!
    const gridSize = 50 //设置网格大小为10;
    const canvasHeight = canvasDom.height
    const 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.width
    const canvasHeight = canvasDom.height
    //确定坐标的长度:
    //确定箭头的大小: 一半3,6,,直角边
    const space = 100
    const arrowSize = 12
    const lineWidth = 2
    const x0 = space
    const y0 = canvasHeight - space
    //画x轴
    ctx.beginPath()
    ctx.moveTo(x0, y0 - 0.5)
    ctx.lineTo(canvasWidth - x0, y0 - 0.5)
    ctx.strokeStyle = '#000'
    ctx.lineWidth = lineWidth
    ctx.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: number
    let boxDomHeight: number
    const fun = (data: any) => {
      if (canvasDom) {
        boxDomWidth = data[0].contentRect.width
        boxDomHeight = data[0].contentRect.height
        // 获取到盒子的宽高之后就把画布的宽高设置成两倍
        canvasDom.height = boxDomHeight * 1
        canvasDom.width = boxDomWidth * 1
        // canvas元素的样式设置成一倍
        canvasDom.style.height = String(boxDomHeight + 'px')
        canvasDom.style.width = String(boxDomWidth + 'px')
        setGrid()
        setCoordinate()
      }
    }
    const observer = new ResizeObserver(fun)
    // 监听父元素大小变化,父元素大小变化就重新画canvas
    observer.observe(boxDom)
  }
  useEffect(() => {
    getOnSize()
  }, [dataArr])
  const setDataFun = () => {
    setDataArr((a) => {
      if (dataArr.length == modelData.length) {
        return a
      }
      return [...a, modelData[dataArr.length]]
    })
  }
  return (
    <canvas
      onClick={() => {
        setDataFun()
      }}
      ref={(a) => {
        canvasRef.current = a
      }}></canvas>
  )
}
export default CanvasLine

结果:
在这里插入图片描述
值是[20,50,20,50,20,50,20]的图形

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值