数据可视化 -- 封装自己的 Charts

myCharts 目录结构
在这里插入图片描述

myCharts 工程化配置

  • webpack.config.js

    const path = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
      mode: "development",
      entry: "./src/index.js",
      output: {
        filename: 'chart.js',
        path: path.resolve(__dirname, 'dist')
      },
      devServer: {
        port: 5000
      },
      module: {
        rules: [
          {
            test: /\.css/,
            use: ['style-loader', 'css-loader']
          },
          {
            test: /\.js/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  presets: ['@babel/preset-env']
                }
              }
            ]
          }
        ]
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: './public/index.html'
        })
      ]
    }
    
  • public/index.html

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>可视化插件封装</title>
      <script>
        document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px'
        window.addEventListener('resize', () => {
          document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px'
        })
      </script>
    </head>
    
    <body>
      <div class="box"></div>
    </body>
    
    </html>
    

myCharts初始化

  • src/index.js

    import '../css/main.css'
    import MyCharts from './charts'
    
    new MyCharts({
      select: '#box1',
      ratio: 1.5,
      type: 'cirque'
    })
    
  • src/charts.js

    import utils from './utils'
    
    class MyCharts {
      constructor(defaultParam) {
        this.defaultParam = defaultParam
        this._canvasParDom = document.querySelector(this.defaultParam.select)
        this.containerWidth = this._canvasParDom.clientWidth
        this.containerHeight = this._canvasParDom.clientHeight
        this._canvas = document.createElement('canvas')
    
        // 设置默认配置
        this.defaultConfig = {
          styles: {
            borderColor: "#6b9bb8",
            lineColor: '#9ec8da',
            pointColor: '#41627c'
          },
          data: [],
          x: 40,
          padding: 20,
          fontSize: '16px',
          wd: this.containerWidth * this.defaultParam.ratio,
          ht: this.containerHeight * this.defaultParam.ratio,
          lineWidth: 2,
          hisColor: ['#7b8c7c', '#5c968a', '#576d93', '#a0d878', '#337d56', '#c1d0ae', '#93b469', '#bda29a']
        }
    
        // 上下文绘制环境
        this.ctx = this._canvas.getContext('2d')
    
        // 缩放画布大小
        this._canvas.width = this.containerWidth * this.defaultParam.ratio
        this._canvas.height = this.containerHeight * this.defaultParam.ratio
    
        // 添加至div 当中
        this._canvasParDom.appendChild(this._canvas)
    
        // 扩展或者覆盖配置
        this.defaultParam = utils.extendsObj(this.defaultConfig, this.defaultParam)
    
        // 设置合适的画布宽度
        this.defaultParam.wid = this._canvas.width - 20
    
        this.init()
      }
    
      init() {
        switch (this.defaultParam.type) {
          case 'cirque':
            console.log('绘制圆环')
            break
          default:
            console.log('无此功能的绘制')
        }
      }
    }
    
    export default MyCharts
    
  • src/utils.js

    let utils = {
      extendsObj: (obj1, obj2) => {
        for (let k in obj2) {
          obj1[k] = obj2[k]
        }
        return obj1
      }
    }
    
    export default utils
    

myCharts动画函数实现

  • src/myAnimation.js

    export default function myAnimation(param) {
      let current = 0
      let looped
      const ctx = this.ctx
      const _canvas = this._canvas
      const callback = param.render
      const successCb = param.success;
      (function looping() {
        looped = requestAnimationFrame(looping)
        if (current < param.percent) {
          ctx.clearRect(0, 0, _canvas.width, _canvas.height)
          current = current + 4 > param.percent ? param.percent : current + 4
          callback(current)
        } else {
          window.cancelAnimationFrame(looping)
          looped = null
          successCb && successCb()
        }
      })()
    }
    

进度园实现

  • src/charts.js

    import Cirque from './cirque'
    import myAnimation from './myAnimation'
    // ...
    init() {
        switch (this.defaultParam.type) {
          case 'cirque':
            let circleConfig = {
              x: this.defaultParam.wd / 2,
              y: this.defaultParam.ht / 2,
              radius: 200,
              startAngle: 0,
              endAngle: 2 * Math.PI,
              arcWidth: 18,
              target: 90
            }
            this.circleConfig = utils.extendsObj(this.defaultConfig, circleConfig)
            myAnimation.call(this, {
              percent: this.circleConfig.target,
              render: (current) => {
                Cirque.call(this, current / 100)
              }
            })
            break
          default:
            console.log('无此功能的绘制')
        }
      }
    
  • src/cirque.js

    let Cirque = function (percent) {
    
      const ctx = this.ctx
      const circleConfig = this.defaultParam
    
      // 绘制打底圆环
      ctx.beginPath()
      ctx.lineWidth = circleConfig.arcWidth
      let grd = ctx.createRadialGradient(circleConfig.x, circleConfig.y, circleConfig.radius - 10, circleConfig.x, circleConfig.y, circleConfig.radius + 10)
      grd.addColorStop(0, "#e9eae9")
      grd.addColorStop("0.8", "#fefefe")
      grd.addColorStop("1", "#e9eae9")
      ctx.strokeStyle = grd
      ctx.arc(circleConfig.x, circleConfig.y, circleConfig.radius, circleConfig.startAngle, circleConfig.endAngle)
      ctx.stroke()
      ctx.closePath()
    
      // 绘制进度圆环
      ctx.beginPath()
      ctx.lineWidth = circleConfig.arcWidth
      let linear = ctx.createLinearGradient(220, 220, 380, 200)
      linear.addColorStop(0, '#ffc26b')
      linear.addColorStop(0.5, '#ff9a5f')
      linear.addColorStop(1, '#ff8157')
      ctx.strokeStyle = linear
      ctx.arc(circleConfig.x, circleConfig.y, circleConfig.radius, circleConfig.startAngle, circleConfig.endAngle * percent)
      ctx.stroke()
      ctx.closePath()
    
      // 起点的圆形
      ctx.beginPath()
      ctx.fillStyle = '#ff7854'
      ctx.arc(circleConfig.x + circleConfig.radius, circleConfig.y - 1, circleConfig.arcWidth / 2, circleConfig.startAngle, circleConfig.endAngle)
      ctx.fill()
      ctx.closePath()
    
      // 终点的圆形
      ctx.beginPath()
      ctx.lineWidth = circleConfig.arcWidth - 10
      ctx.fillStyle = '#fff'
      ctx.strokeStyle = '#ff7854'
      let tarX = circleConfig.x + circleConfig.radius * Math.cos(2 * Math.PI * percent)
      let tarY = circleConfig.y + circleConfig.radius * Math.sin(2 * Math.PI * percent)
      ctx.arc(tarX, tarY, circleConfig.arcWidth - 8, circleConfig.startAngle, circleConfig.endAngle)
      ctx.fill()
      ctx.stroke()
      ctx.closePath()
    
    }
    
    export default Cirque
    

折线图绘制

  • src/index.js

    import '../css/main.css'
    import MyCharts from './charts'
    
    new MyCharts({
      select: '#box2',
      ratio: 1.5,
      type: 'line',
      data: [{
        xVal: '周一',
        yVal: 40
      }, {
        xVal: '周二',
        yVal: 70
      }, {
        xVal: '周三',
        yVal: 30
      }, {
        xVal: '周四',
        yVal: 80
      }, {
        xVal: '周五',
        yVal: 30
      }, {
        xVal: '周六',
        yVal: 20
      }, {
        xVal: '周日',
        yVal: 90
      }]
    })
    
  • src/charts.js

    import utils from './utils'
    import myAnimation from './myAnimation'
    import { drawAxis, drawPoint, drawBrokenLine, drawDashLine } from './broken'
    // ...
     init() {
        switch (this.defaultParam.type) {
          case 'line':
            myAnimation.call(this, {
              percent: 200,
              render: (current) => {
                // 绘制坐标系
                drawAxis.call(this)
                // 绘制虚线
                drawBrokenLine.call(this, current / 200)
                // 绘制Y轴虚线
                drawDashLine.call(this, current / 200)
                // 绘制圆形
                drawPoint.call(this, current / 200)
              }
            })
            break;
          default:
            console.log('无此功能的绘制')
        }
      }
    
  • src/broken.js

    export function drawAxis() {
      const defaultParam = this.defaultParam
      const ctx = this.ctx
      const pad = defaultParam.padding
      const bottomPad = 30
      const wd = defaultParam.wd
      const ht = defaultParam.ht
      const data = defaultParam.data
    
      // 绘制坐标系
      ctx.save()
      ctx.beginPath()
      ctx.lineWidth = 2
      ctx.strokeStyle = defaultParam.styles.borderColor
      ctx.moveTo(pad, pad)
      ctx.lineTo(pad, ht - bottomPad)
      ctx.lineTo(wd - pad, ht - bottomPad)
      ctx.stroke()
      ctx.closePath()
    
    
      // 绘制文字刻度 
      for (let i = 0; i < data.length; i++) {
        ctx.beginPath()
        ctx.fillStyle = '#333'
        ctx.textAlign = 'center'
        ctx.font = defaultParam.fontSize + ' Microsoft YaHei'
        ctx.fillText(data[i].xVal, i * (defaultParam.wid / data.length - 1) + defaultParam.x, ht - 10)
        ctx.closePath()
      }
    
      ctx.restore()
    }
    
    export function drawPoint(speed) {
      const defaultParam = this.defaultParam
      const ctx = this.ctx
      const data = defaultParam.data
      const len = data.length
      const ht = defaultParam.ht
    
      // 计算
      ctx.save()
      ctx.lineWidth = 2
      for (let i = 0; i < len; i++) {
        let yVal = parseInt(data[i].yVal * speed)
        let tranY = ht - ht * yVal / defaultParam.maxPoint - 30
        let tranX = i * (defaultParam.wid / len - 1) + defaultParam.x
    
        // 绘制图形
        ctx.beginPath()
        ctx.shadowOffsetX = 0
        ctx.shadowOffsetY = 0
        ctx.shadowBlur = 3
        ctx.shadowColor = defaultParam.styles.pointColor
        ctx.fillStyle = defaultParam.styles.pointColor
        ctx.strokeStyle = '#fff'
        ctx.arc(tranX, tranY, 6, 0, 2 * Math.PI, false)
        ctx.fill()
        ctx.stroke()
        ctx.closePath()
    
        // 绘制圆形对应的数值
        ctx.beginPath()
        ctx.shadowBlur = 0
        ctx.fillStyle = '#333'
        ctx.textAlign = 'center'
        ctx.font = defaultParam.fontSize + ' MicroSoft YaHei'
        ctx.fillText(yVal, tranX, tranY - 10)
        ctx.closePath()
    
      }
      ctx.restore()
    }
    
    export function drawBrokenLine(speed) {
      const defaultParam = this.defaultParam
      const ctx = this.ctx
      const bottomPad = 30
      const data = defaultParam.data
      const ht = defaultParam.ht
      const maxPoint = defaultParam.maxPoint
      const len = data.length - 1
      const stepDots = Math.floor(speed * len)
    
      // 绘制线条 
      ctx.save()
      ctx.beginPath()
      ctx.setLineDash([4, 4])
      ctx.lineWidth = defaultParam.lineWidth
      ctx.strokeStyle = defaultParam.styles.lineColor
      for (let i = 0; i < len; i++) {
        // 起点 
        const yVal = data[i].yVal
        const axisY = ht - ht * (yVal / maxPoint) - bottomPad
        const averageNum = defaultParam.wid / data.length - 1
        const axisX = i * averageNum + defaultParam.x
    
        // 终点 
        let axisEndX = (i + 1) * averageNum + defaultParam.x
        let axisEndY = ht - ht * (data[i + 1].yVal) / maxPoint - bottomPad
    
        if (i <= stepDots) {
          if (i === stepDots) {
            axisEndX = (axisEndX - axisX) * speed + axisX
            axisEndY = (axisEndY - axisY) * speed + axisY
          }
          ctx.moveTo(axisX, axisY)
          ctx.lineTo(axisEndX, axisEndY)
        }
      }
      ctx.stroke()
      ctx.closePath()
      ctx.restore()
    }
    
    export function drawDashLine(speed) {
      const defaultParam = this.defaultParam
      const ctx = this.ctx
      const bottomPad = 30
      const data = defaultParam.data
      const ht = defaultParam.ht
      const maxPoint = defaultParam.maxPoint
      const len = data.length
    
      ctx.save()
      for (let i = 0; i < len; i++) {
        // 起始点
        const averageNum = defaultParam.wid / data.length - 1
        let axisX = i * averageNum + defaultParam.x
        let axisY = ht - ht * (data[i].yVal) / maxPoint * speed - bottomPad
    
        // 开始绘制 
        ctx.beginPath()
        ctx.lineWidth = 2
        ctx.setLineDash([4, 4])
        ctx.strokeStyle = '#d6d6d6'
        ctx.moveTo(axisX, ht - bottomPad)
        ctx.lineTo(axisX, axisY)
        ctx.stroke()
        ctx.closePath()
      }
      ctx.restore()
    }
    

直方图绘制

  • src/index.js

    import '../css/main.css'
    import MyCharts from './charts'
    
    new MyCharts({
      type: 'histogram',
      select: '#box3',
      ratio: 1.5,
      data: [{
        xVal: 'vue',
        yVal: 80
      }, {
        xVal: 'react',
        yVal: 70
      }, {
        xVal: 'angular',
        yVal: 40
      }, {
        xVal: 'webpack',
        yVal: 90
      }, {
        xVal: '2222',
        yVal: 80
      }, {
        xVal: 'typescript',
        yVal: 40
      }, {
        xVal: 'ES6+',
        yVal: 100
      }]
    })
    
  • src/charts.js

    import utils from './utils'
    import Cirque from './cirque'
    import myAnimation from './myAnimation'
    import { drawHistogram } from './histogram'
    import { drawAxis, drawPoint, drawBrokenLine, drawDashLine } from './broken'
    // ...
     init() {
        switch (this.defaultParam.type) {
          case 'cirque':
            // ...
            break
          case 'line':
            // ...
            break;
          case 'histogram':
            myAnimation.call(this, {
              percent: 100,
              render: (current) => {
                // 绘制坐标系
                drawAxis.call(this)
                // 绘制直方图
                drawHistogram.call(this, current / 100)
              }
            })
            break
          default:
            console.log('无此功能的绘制')
        }
      }
    
  • src/histogram.js

    export function drawHistogram(speed) {
      const defaultParam = this.defaultParam
      const ctx = this.ctx
      const bottomPad = 30
      const data = defaultParam.data
      const ht = defaultParam.ht
      const maxPoint = defaultParam.maxPoint
      const len = data.length
      let rectHeight = this._canvas.height - bottomPad
    
      for (let i = 0; i < len; i++) {
        let yVal = data[i].yVal * speed
        let axisY = ht - ht * (yVal / maxPoint) - bottomPad
        const averageNum = defaultParam.wid / data.length - 1
        let axisX = i * averageNum + defaultParam.x
    
        ctx.save()
        ctx.beginPath()
        ctx.fillStyle = defaultParam.hisColor[i]
        ctx.fillRect(axisX - 15, axisY, 30, rectHeight - axisY)
        ctx.restore()
      }
    
    }
    
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值