canvas之旅系列----(四)

484 阅读4分钟

canvas绘制南丁格尔玫瑰图

利用canvasApi来绘制一个南丁格尔玫瑰图,效果如下。gitee源码github源码

南丁格尔玫瑰图绘制

创建绘图对象

创建一个绘图对象用于存储必要信息和管理绘图

/**
 * 初始化图形
 * @param {string} id canvas的id 
 * @param {Array} data 数据 
 */
function PieChart(id, data) {
  const canvas = document.getElementById(id)
  const context = canvas.getContext('2d')
  // 赋值属性
  this._canvas = canvas
  this._context = context
  // 定义错误信息,在出现错误信息的时候就直接清空画布(响应式的)
  let error = ""
  Object.defineProperty(this, '_error', {
    enumerable: false,
    configurable: false,
    set(val) {
      if (typeof val === 'string' && val !== "") {
        console.error(val)
        error = val
        if(this._clear) this._clear()
      } else {
        error = ""
      }
    },
    get() {
      return error
    }
  })
  this._maxValue = 0
  this._minR = 0 // 中间白色圆圈半径
  this._maxR = 0 // 最大半径
  this._cache = [] // 离屏canvas图像存储
  this.data = this._mapData(data) //数据
  this._last = null // 记录上一个选中的区域
  _bindHover(this) // 绑定交互事件
}

数据准备

在该系列的前几篇文章绘制了折线图和柱状图,其实很容易能够总结出,在canvas的绘制过程中,一个重点就是位置坐标的计算。此次我们绘制图形的数据为:

let data = [
  {lable: 'A', value: 10},
  {lable: 'B', value: 5},
  {lable: 'C', value: 20},
  {lable: 'D', value: 40},
  {lable: 'E', value: 30},
]

为了方便后续绘制,所以我们要先对数据进行格式化。

/**
 * 获取扇形区域的两个顶点
 */
PieChart.prototype._mapData = function(data) {
  if (this._error) return data
  let chartZone = this._getChartZone()
  if (chartZone == 0) return []
  let center = (chartZone[0] + chartZone[1]) / 2 //获取图形中心
  let _r = (chartZone[1] - chartZone[0]) / 2 * 0.8 //参考半径值,用来计算半径
  this._minR = _r / 4 > 20 ? 20 :  _r / 4
  this._maxR = _r
  let count = 0
  let angle = 0
  data.forEach((item) => {
    if (item.value <=0 ) this._error = '数据value值应当为大于0的值'
    if (item.value > this._maxValue) this._maxValue = item.value
    count += item.value
  })
  let _R = Math.sqrt((this._maxR**2 - this._minR**2) * count / this._maxValue + this._minR**2)//参考值
  data = data.map((item, index) => { //格式化数据
    item.percent = item.value / count //所占比例
    item.startAngle = angle //起始角度
    item.angle = 2 * Math.PI * item.percent //占的角度
    angle += 2 * Math.PI * item.percent
    item.color = _getColor(159, 100, 200, index) // 计算一个颜色
    item.R = Math.sqrt((_R**2 - this._minR**2) * item.percent + this._minR**2) // 计算半径
    return item
  })
  return data
}

懒得想颜色怎么搭配,就随手写了个颜色计算的小函数,颜色的计算依据初始rgb值计算,尽量避免重复颜色即可。也可以自己写上颜色,当然,数据项多了,自己给每个写个颜色就有点不现实了。

在半径的计算中,半径之间的关系如下图所示:

关系

图形绘制

处理完数据后,接下来就是图形的绘制

/**
 * 绘制南丁格尔玫瑰图
 */
PieChart.prototype.draw = function() {
  if (this._error) return this
  this._clear()
  let chartZone = this._getChartZone() // 获取绘图区域
  if (chartZone == 0) return this
  let center = (chartZone[0] + chartZone[1]) / 2 // 求绘图中心
  this.data.forEach((item, index) => { // 绘制每个扇形
      this._context.beginPath()
      this._context.moveTo(center, center)
      this._context.arc(center, center, item.R, item.startAngle, item.startAngle + item.angle, false)
      this._context.closePath()
      this._context.fillStyle = item.color.str
      this._context.fill()
  })
  // 绘制中心空白区域
  this._context.beginPath()
  this._context.arc(center, center, this._minR, 0, 2 * Math.PI, false)
  this._context.fillStyle = "#ffffff"
  this._context.fill()
  return this
}

交互效果

像素操作

交互效果的实现,主要问题是对象的选取判断。在本系列的第三篇文章中提到利用坐标的对比来判断鼠标位置是否落在了对象上,这次我们采用一个新的方式来判断。此处要用到canvas的像素操作。canvas的getImageData的借口可以创建一个ImageData对象,其中存储了图片每个点的rgba值,存储方式为一个无符号8位整形数组,每个点的rgba占4个值,依次位r、g、b、a的值,都为0-255。我们可以通过这个数组访问到每个像素的颜色值,因为扇形的颜色是唯一的,所以只要相互对比,就能知道鼠标是否落在对象上了。

关于canvas的像素操作,详细可参考MDN文档

添加交互效果

/**
 * 交互
 */
function _bindHover(chart) {
  if (!chart._canvas) return
  chart._canvas.onmousemove = (e) => {
    let offsetX = e.offsetX
    let offsetY = e.offsetY
    let imageData = chart._context.getImageData(0, 0, chart._canvas.width, chart._canvas.height)
    let rgba = {}
    let base = (offsetY) * imageData.width * 4 + (offsetX) * 4
    //获取鼠标点的颜色
    rgba.R = imageData.data[base + 0]
    rgba.G = imageData.data[base + 1]
    rgba.B = imageData.data[base + 2]
    rgba.A = imageData.data[base + 3]
    let flag = true
    for(let item of chart.data) {
      let color = item.color
      if (color.R === rgba.R && color.G === rgba.G && color.B === rgba.B) {
      	//颜色对比,选中对象
        flag = false
        if (chart._cache.length === 0) {
          chart._cache.push(chart._canvas.toDataURL('image/png'))
        }
        if (chart._cache.length === 0 || chart._last !== item.lable) {
          if(chart._last !== item.lable) {
            //确保会先清除后再画
            chart._last = item.lable
            _drawCache(chart, () => {
              _drawLable(chart, item) //绘制标签
            })
          } else {
            _drawLable(chart, item)
          }
          break
        }
      }
    }
    if (flag) {
    //未选中任何对象
      if(chart._cache.length > 0) {
        chart._last = null
        _drawCache(chart) //绘制离屏存储的图像
      }
    }
  }
}

总结

此次南丁格尔玫瑰图的绘制主要是为再次熟悉canvas的基本绘制流程。在整个绘制过程中,引入了新的点就是canvas的像素操作。在css3中也有了滤镜的操作,可以对图片添加一些滤镜来处理图片,但是css3的滤镜可能在一些低版本的浏览器下存在问题,canvas的像素操作就可以用来很好的做一个兼容。有兴趣可以去了解css3的滤镜,在思考下如果是canvas可以如何来实现这些滤镜功能。