前端高级之路:写一个高逼格可视化“圆环

开发
日常生产生活中,我们会经常读到或使用各种类型的图表。圆环(圆弧)便是一种较常见的类型,用于直观展现某一数据指标占整体的比例。本文以 HTML Canvas 的实现为主(当然,SVG 党可以在了解原理后自行实现),逐层介绍圆环图表开发的一些主要思路和原理。

图1 所示是一些我们平时比较常见的一些圆环(圆弧)效果。虽然图形的主体构成都是圆弧,但不同效果在信息传达的功能上却略有差异。如:

  • 闭合的圆环可以表示流程 “进度” 的概念
  • 非闭合圆环一般用于状态量(标量)的展示,一般也称为 “仪表盘” 效果
  • 不同的色相可用于标识状态量的状态区间(如:低危-中危-高危 区间的标识可以使用三种颜色的构成的过渡效果进行表达)

为了更加方便、完善地解决我们在业务开发时的具体需要,可以对这些风格、样式进行一定分析、抽象,总结出一个通用组件需要具备的能力,如:

  • 颜色、渐变可配(支持传入单一色值或颜色序列)
  • 圆弧宽度可调(内、外半径大小可配置)
  • 圆弧夹角可调(开始角度、截止角度可配置)
  • 圆弧端点造型可选(可切换平角/半圆造型
  • 文案效果可调(字号、字体、颜色等)

下面,我们着手于实现这样一个功能全面、业务通用性较强的圆环组件。

1. 圆环的造型
绘制圆环造型的第一步,需要先绘制圆环图表构成要素,即一段一段的圆弧。而对于像下图中这样的两种倒角效果(黄色部分圆弧两端的样式),既可以是直角,也可以是半圆。 

因此,我们需要实现一个通用的方法来绘制圆弧,提供两种倒角风格给用户。

1.1 前景圆弧的绘制

圆弧绘制的思路如上图所示,按先后顺序大致分为几个步骤:

(1)绘制圆弧起始端的半圆轮廓

(2)绘制圆弧的外边缘轮廓

(3)绘制圆弧终止端的半圆轮廓

(4)绘制圆弧的内边缘轮廓

(5)闭合轮廓并填充色彩

注:由于 canvas 绘制圆弧的方法默认是顺时针方向,因而我们的绘图步骤也是沿着顺时针方向

以下是一些姿势要领:

1.1.1 端点坐标的计算
在绘制端点半圆之前我们需要端点的位置坐标,以其实端为例,根据圆环的半径(内外径的均值,即圆弧中线的半径)和起始端点的角度如何计算圆上一点的坐标:

// 计算圆弧上某点的坐标 
// originX, originY - 圆心的坐标 
// radius - 圆环半径,等于圆环内、外径的平均值,也即圆弧中线的半径 
// alpha - 弧度 
function calcPosition(originX, originY, radius, alpha) { 
  return [ 
    radius * Math.cos(alpha) + originX, 
    radius * Math.sin(alpha) + originY, 
  ]; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

1.1.2 端点半圆的起止角度

在 canvas 中绘制一个 arc,需要知道其起始角度和终止角度。由于 canvas 绘制默认方向为屏幕顺时针方向(屏幕 Z轴 的左手螺旋方向),从上面的示意图中可以看出:

起始端半圆弧度范围 - [radianStart - Math.PI, radianStart]

终止端半圆弧度范围 - [radianEnd, radianEnd + Math.PI]

1.1.3 绘制半圆
有了端点坐标和起止角度,便可以绘制端点的半圆:

// 以起始端的半圆倒角为例 
myCanvas.context.arc( 
  x, 
  y, 
  (radiusOutter - radiusInner) / 2,     // 小圆半径,等于圆环线宽的一半 
  radianStart - Math.PI, 
  radianStart 
); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

直角倒角风格的绘制与半圆倒角圆弧的绘制步骤基本相同,主要差别在于不用绘制圆弧两个端点的小半圆,改成绘制直线。背景圆弧的绘制也与前景圆弧方法一致。

2. Canvas 实现锥形渐变
上面的步骤可以绘制出圆弧的轮廓,要达到 图1 那样的视觉效果,我们需要给前面绘制出来的轮廓填充图像。

沿着圆周方向的渐变,因为其图像形似圆锥体的俯瞰效果,俗称锥形渐变:

众所周知,CSS 中有一个名为 conic-gradient 的属性直接支持锥形渐变,而 HTML Canvas 的原生 API 目前还没有类似的能力。那么,我们如何在 canvas 中绘制出这样的图像呢?

下面我们讲下大致的原理:

(1)对用户传入的颜色进行插值,得到一个颜色序列。

这里,我们直接使用 canvas 原生的 createLinearGradient 方法,在离屏 canvas 中绘制一个 1px 的线性渐变效果,图像宽度正好是我们要插值的数量,渐变插值的结果也就是 canvas 上对应像素位置的色值。

 

颜色插值(渐变取色)代码实现如下:

// 用于实现颜色插值的工具类 
export default class ColorInterpolate { 
  // 参数01: stops - 为要插值的颜色序列,数据格式形如:[[0, 'red'], [0.5, 'green'], [1.0, 'yellow']] 
  // 参数02: segment - 插值段落数,即插值结果的颜色值的数量 
  constructor(stops = [], segment = 100) { 
    // 构建离屏 canvas 
    const canvas = document.createElement('canvas'); 
    canvas.width = segment; 
    canvas.height = 1; 
    this.ctx = canvas.getContext('2d'); 
 
    // 绘制线性渐变 
    const gradient = this.ctx.createLinearGradient(0, 0, segment, 0); 
    for (let [offset, color] of stops) { 
      gradient.addColorStop(offset, color); 
    } 
 
    this.ctx.fillStyle = gradient; 
    this.ctx.fillRect(0, 0, segment, 1); 
  } 
 
  // 根据位置偏移量获取插值后的色值 
  getColor(offset) { 
    const imgData = this.ctx.getImageData(offset, 0, 1, 1); 
    return `rgba(${imgData.data.slice(0, 3).join(',')}, ${imgData.data[3] / 255})`; 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

(2)如下图所示,我们可以把渐变的图像看成是由足够多填充了单个色值的小 “扇面” 拼接而成。

按照这样的思路,我们只需要遍历上面色彩插值得到的各个颜色,然后逐个绘制小扇面,便可得到一个锥形渐变图像。

为此我们封装了一个名为 createConicalGradient 的方法,其使用习惯与 canvas 原生的 createLinearGradient 和 createRadialGradient 方法相似。具体代码见 我的 Github(觉得有用的童鞋可以 star 一下)。

3. 过渡动画
3.1 圆弧的过渡
在数值发生改变时,我们的图表需要一个能够跟随数据改变的过渡动画效果,对于 canvas 而言,便是清除旧图像然后绘制新一帧图像。这里有一些方法包装上的技巧:

// 注:伪代码,真实场景建议 OOP 方式包装为工具类 
 
let _animTick = null
let _animFrames = null
let _frameData = null
let _animDiff = null
 
// 动画方法 
function _animate(duration) { 
  if (_animTick === null) { 
    // 根据动画时长 duration 计算整个动画一共需要多少帧(以 60fps 计算) 
    _animFrames = Math.round((duration / 1e3) * 60); 
 
    // 相邻两帧动画的数据变化 
    _animDiff = _calcAnimDiff(_animFrames); 
 
    // 动画帧数标识 
    _animTick = 0; 
  } 
 
  // 当前帧的数据值 
  _frameData = _caclCurentData(_animDiff, _animTick); 
  _renderFrame(_frameData); 
 
  if (_animTick !== null && _animTick < _animFrames) { 
    // 继续执行动画 
    window.requestAnimationFrame(() => { 
      _animate(); 
      _animTick += 1; 
    }); 
  } else { 
    // 动画结束 
    _renderFrame(_frameData); 
    _animTick = null
  } 

 
// 绘制当前帧 
function _renderFrame(data) { 
  // ... 

 
// 计算动画相邻帧的数据差异 
function _calcAnimDiff() { 
  // ... 

 
// 根据两帧数据差计算当前帧 
function _caclCurentData() { 
  // ... 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.

3.2 两种动画模式
在过渡动画执行的过程中,需要考虑两种不同的模式:一种是渐变图像不变化,仅是圆弧的轮廓从旧状态变化到新状态;一种是渐变图像的夹角范围跟随轮廓的大小改变。

这两种模式其实都有一定意义:前者可以使用不同颜色代表数值的不同状态;后者仅仅是将渐变的颜色看成一种装饰效果。

4. 结果演示
比较关键的原理都介绍完了,最后展示一下我们封装的图表组件的效果(右侧 GUI 部分是我们自研的设计引擎的编辑效果):

 

 

责任编辑:姜华 来源: 晨曦大前端
相关推荐

2016-11-17 12:49:36

云运维银行卡建设

2017-06-19 08:30:35

大数据数据可视化报表

2020-06-08 15:18:50

Python图片PIL

2024-05-22 16:03:49

2020-04-10 14:20:47

算法可视化Github

2024-03-11 00:05:00

2020-03-11 14:39:26

数据可视化地图可视化地理信息

2021-08-11 06:57:17

验证码图片显示

2017-07-10 14:18:34

微服务架构可视化

2021-06-09 11:26:37

BokehPython可视化

2013-12-06 10:05:29

数据中心网络操作可视化

2019-10-09 17:12:16

PythonLinuxWindows

2017-10-14 13:54:26

数据可视化数据信息可视化

2022-04-20 20:30:36

可视化模块Python

2015-04-01 10:07:06

云计算概念公有云私有云

2025-02-14 00:25:00

SQL写法业务

2022-07-07 08:50:26

Python可视化模块代码

2017-08-17 14:20:35

大数据可视化方法

2021-03-31 13:28:17

开源工具Python编程语言

2022-08-26 09:15:58

Python可视化plotly
点赞
收藏

51CTO技术栈公众号