我们打开 G2 看到的仪表盘如下图左边的样子,但是这个仪表盘看起来有点生硬。下图中右边的仪表盘是不是样子更炫酷了一些呢?那么如何绘制一个这样的仪表盘呢?接下来我们就来剖析一下这样的一个仪表盘是怎样做出来的,并在文章最后附上全部代码供大家参考。

G2 定制仪表盘实践 - 图1

指针的绘制

分析 G2 仪表盘核心代码 源代码请看这里

  1. var Shape = G2.Shape;
  2. // 自定义Shape 部分
  3. Shape.registerShape('point', 'pointer', {
  4. drawShape: function drawShape(cfg, group) {
  5. var center = this.parsePoint({ // 获取极坐标系下画布中心点
  6. x: 0,
  7. y: 0
  8. });
  9. // 绘制指针
  10. group.addShape('line', {
  11. attrs: {
  12. x1: center.x,
  13. y1: center.y,
  14. x2: cfg.x,
  15. y2: cfg.y,
  16. stroke: cfg.color,
  17. lineWidth: 5,
  18. lineCap: 'round'
  19. }
  20. });
  21. return group.addShape('circle', {
  22. attrs: {
  23. x: center.x,
  24. y: center.y,
  25. r: 9.75,
  26. stroke: cfg.color,
  27. lineWidth: 4.5,
  28. fill: '#fff'
  29. }
  30. });
  31. }
  32. });

该部分自定义了仪表盘的指针形状,由一条线和一个圆圈组合而成。

drawShape 函数中第一个参数 cfg 中携带了数据当前点坐标和颜色等信息,从而指针的指向会随数据指标而变化。

那么重写该部分,就可以将 G2 原生的指针变成我们想要的样子了。但是不变的是指针的指向需要随数据指标而变化。如此一来,绘制一个箭头型指针就变成了一个数学问题:已知圆心(xc, yc),圆上任意一点 (x, y),绘制一个箭头型指针从 (xc, yc) 指向 (x,y)

解题步骤

以仪表盘的原心坐标为原心建立坐标系如图,求出关键点 (x0, y0)(x1, y1)(x2, y2)(x3, y3)的位置。

G2 定制仪表盘实践 - 图2

(1)首先(x0, y0)(x2, y2) 两个点都在由 (xc, yc)(x, y) 两点组成的线段上,所以 :

(x0-xc, y0-yc) = λ1(x-xc, y-yc) => (x0, y0) = (λ1*(x-xc) + xc, λ1*(y-yc) + yc)

(x2-xc, y2-yc) = λ2(x-xc, y-yc) => (x2, y2) = (λ2*(x-xc) + xc, λ2*(y-yc) + yc)

  1. 其中 λ12 都是可调节的参数在01范围内,它们决定了箭头的起始和终止位置。

(2)然后计算 (x1, y1)

  1. `(x1, y1)` `(xc, yc)` `(x,y)` 组成的线段做一条垂直线段,设该线段的长度是 `d`(如图红色部分),垂直点为 `(xd, yd)`
  2. 与步骤一同理,`(xd, yd)` 是由 `(xc, yc)``(x, y)` 两点组成的线段上的点,所以:
  3. `(xd-xc, yd-yc) = λd(x-xc, y-yc) => (xd, yd) = (λd*(x-xc) + xc, λd*(y-yc) + yc)`
  4. 线段 `d` 的倾斜角度与向量 `(x ,y)` 存在某种关系,冥冥中我们能感觉到,通过 `(xd, yd)` `d`我们能计算出 `(x1, y1)` 的位置,那么如何计算呢?
  5. 我们以 `(xd, yd)` 为原心,再次建立坐标系,如下图:

G2 定制仪表盘实践 - 图3

  1. 放大该新的坐标系,并建立辅助线:从 `(x1, y1)` 向横坐标轴做一条垂直线段如下图:

G2 定制仪表盘实践 - 图4

  1. 图中出现了三个相等的角,如图蓝色的部分,所以采用三角函数可得:

计算x1
  1. sin α1 = sin α3 => (xd - x1) / d = (y-yc) / √( (x-xc + (y-yc)²)
  2. `xd = λd * (x-xc) + xc` `√( (x-xc)² + (y-yc)²) = r` 带入上式得到:

(λd * (x-xc) + xc - x1) / d = (y-yc) / r

  1. => x1 = λd * (x-xc) + xc - (d/r) *( y - yc)
  2. 那么 `x1` 就通过 `(x, y)``(xc, yc)` 表达出来了。

计算 y1
  1. cos α1 = cos α3 => (y1 - yd) / d = (x-xc) / √( (x-xc + (y-yc)²)
  2. `yd = λd * (y-yc) + yc` `√( (x-xc)² + (y-yc)²) = r` 带入上式得到:
  3. (y1-(λd * (y-yc) + yc )) / d = (x-xc) / r
  4. => y1 = λd * (y-yc) + yc + (d/r) *( x - xc)

(3)计算 (x3, y3)

  1. 与(2)中同理,从 `(x3, y3)` `(xc, yc)` `(x,y)` 组成的线段做一条垂直线段,设该线段的长度是 `d`(如图红色部分),垂直点为 `(xd, yd)`。如下图:

G2 定制仪表盘实践 - 图5

由于箭头型指针是一个对称图形,由数学知识可知 改(xd, yd) 点即为(2)中的 (xd, yd) 点:

  1. `(xd-xc, yd-yc) = λd(x-xc, y-yc) => (xd, yd) = (λd*(x-xc) + xc, λd*(y-yc) + yc)`

此时,三个相等的角α1α2α3的位置变为上图蓝色所示,

计算x3
  1. sin α1 = sin α3 => (x3 - xd) / d = (y-yc) / √( (x-xc + (y-yc)²)

xd = λd * (x-xc) + xc√( (x-xc)² + (y-yc)²) = r 带入上式得到:

  1. (x3 - d * (x-xc) + xc)) / d = (y - yc) / r
  2. => x3 = λd * (x-xc) + xc + d/r * (y - yc)

计算y3
  1. cos α1 = cos α3 => (yd - y3) / d = (x-xc) / √( (x-xc + (y-yc)²)

yd = λd * (y-yc) + yc√( (x-xc)² + (y-yc)²) = r 带入上式得到:

  1. d * (y-yc) + yc - y3) / d = (x-xc) / r
  2. => y3 = λd * (y-yc) + yc - d/r * (x-xc)

至此,(x0, y0)(x1, y1)(x2, y2)(x3, y3) 四个关键点的位置已经全部计算出来:

  1. x0 = λ1 * (x-xc) + xc;
  2. y0 = λ1 * (y-yc) + yc;
  3. x1 = λd * (x-xc) + xc - (d/r) * (y - yc);
  4. y1 = λd * (y-yc) + yc + (d/r) * (x - xc);
  5. x2 = λ2 * (x-xc) + xc;
  6. y2 = λ2 * (y-yc) + yc;
  7. x3 = λd * (x-xc) + xc + d/r * (y - yc);
  8. y3 = λd * (y-yc) + yc - d/r * (x - xc);

上列算式中:λ1、λ2 分别是决定指针的起点、终点位置,应介于0、1之间,分别取 0.44、0.55;

  1. λd `(xd, yd)` 的位置,决定箭头的折角位置,该值应介于λ1、λ2之间,靠近 λ1 0.46
  2. (d/r) 决定指针的胖瘦,取 0.012
  3. xy在接下来的代码中应为 cfg.xcfg.y
  4. 圆心`(xc, yc)` 中的 xc yc 在接下来的代码中分别为 center.x center.y

应用到代码中去:

  1. Shape.registerShape('point', 'pointer', {
  2. drawShape: function drawShape(cfg, group) {
  3. const center = this.parsePoint({
  4. // 获取极坐标系下画布中心点
  5. x: 0,
  6. y: 0,
  7. });
  8. // 绘制指针
  9. const x0 = (cfg.x - center.x) * 0.44 + center.x;
  10. const y0 = (cfg.y - center.y) * 0.44 + center.y;
  11. const x1 = (cfg.x - center.x) * 0.46 + center.x - (cfg.y - center.y) * 0.012;
  12. const y1 = (cfg.y - center.y) * 0.46 + center.y + (cfg.x - center.x) * 0.012;
  13. const x2 = (cfg.x - center.x) * 0.55 + center.x;
  14. const y2 = (cfg.y - center.y) * 0.55 + center.y;
  15. const x3 = (cfg.x - center.x) * 0.46 + center.x + (cfg.y - center.y) * 0.012;
  16. const y3 = (cfg.y - center.y) * 0.46 + center.y - (cfg.x - center.x) * 0.012;
  17. group.addShape('path', {
  18. attrs: {
  19. path: `M ${x0} ${y0} L ${x1} ${y1} L ${x2} ${y2} L ${x3} ${y3} Z`,
  20. lineWidth: 10,
  21. lineJoin: 'dot',
  22. stroke: '#5571F7',
  23. },
  24. });
  25. return group.addShape('circle', {
  26. attrs: {
  27. x: center.x,
  28. y: center.y,
  29. r: 3,
  30. stroke: '#5571F7',
  31. lineWidth: 4.5,
  32. fill: '#5571F7',
  33. },
  34. });
  35. },
  36. });

测试办法将上述代码,替换掉 G2 测试代码中的相应部分,运行查看效果。改造前后的样子如下:

G2 定制仪表盘实践 - 图6

形状的改变

观察文章开篇两个图表起始弧度与截止弧度也有差异:

  1. chart.coord('polar', {
  2. startAngle: -9 / 8 * Math.PI,
  3. endAngle: 1 / 8 * Math.PI,
  4. radius: 0.75
  5. });

修改为:

  1. chart.coord('polar', {
  2. startAngle: -10 / 8 * Math.PI,
  3. endAngle: 2 / 8 * Math.PI,
  4. radius: 0.75
  5. });

G2 定制仪表盘实践 - 图7

数据改变

从 0 到 9 改变到 0 到 100

  1. chart.scale('value', {
  2. min: 0,
  3. max: 9,
  4. tickInterval: 1,
  5. nice: false
  6. });

修改为:

  1. chart.scale('value', {
  2. min: 0,
  3. max: 100,
  4. tickInterval: 1,
  5. nice: false
  6. });

但是你会发现一个问题,仪表盘从下图中左边的样子变为右边的样子,背景颜色少了一大半,究其原因是什么呢?我们接下来看圆弧的绘制。

G2 定制仪表盘实践 - 图8

圆弧的绘制

分析 G2 中圆弧的绘制部分,分为两步:仪表盘灰色背景的绘制,指标数据的绘制。

  1. // 绘制仪表盘背景
  2. chart.guide().arc({
  3. zIndex: 0,
  4. top: false,
  5. start: [0, 0.945],
  6. end: [9, 0.945],
  7. style: { // 底灰色
  8. stroke: '#CBCBCB',
  9. lineWidth: 18
  10. }
  11. });
  12. // 绘制指标
  13. chart.guide().arc({
  14. zIndex: 1,
  15. start: [0, 0.945],
  16. end: [data[0].value, 0.945],
  17. style: {
  18. stroke: '#1890FF',
  19. lineWidth: 18
  20. }
  21. });

采用绘制辅助弧线的方式绘制圆弧,start、end 分别表示圆弧的起始位置。其中 end: [9, 0.945],数组中第一项表示 value 维度,的二项表示半径维度。所以在 value 从 0 到 9,变为 0 到 100 时,灰色背景圆弧的截止位置应变为 end: [100, 0.945]。

  1. // 绘制仪表盘背景
  2. chart.guide().arc({
  3. zIndex: 0,
  4. top: false,
  5. start: [0, 0.945],
  6. end: [100, 0.945],
  7. style: { // 底灰色
  8. stroke: '#CBCBCB',
  9. lineWidth: 18
  10. }
  11. });
  12. // 绘制指标
  13. chart.guide().arc({
  14. zIndex: 1,
  15. start: [0, 0.945],
  16. end: [data[0].value, 0.945],
  17. style: {
  18. stroke: '#1890FF',
  19. lineWidth: 18
  20. }
  21. });

G2 定制仪表盘实践 - 图9

仪表盘背景色已经绘制完成,再观察指标绘制就是另一段圆弧的叠加。start、end 分别为圆弧的起始、截止位置,style 中的 lineWidth 为圆弧的厚度。

绘制阴影和弧线

以上绘制背景和指标的方式,即为圆弧叠加,绘制外圈的阴影和弧线同样可用此方式。

追加一段圆弧,用来表示外圈浅灰色阴影:

  1. chart.guide().arc({
  2. zIndex: 1,
  3. start: [0, 1.15],
  4. end: [100, 1.15],
  5. style: {
  6. stroke: '#F5F7FB',
  7. lineWidth: 18
  8. }
  9. });

分段绘制外圈4段弧线:

  1. // 绘制第一段弧线 value 从 2 到 23 空出 2 个value 的位置显示 label
  2. chart.guide().arc({
  3. zIndex: 1,
  4. start: [2, 1.5],
  5. end: [23, 1.5],
  6. style: {
  7. stroke: '#F5F7FB',
  8. lineWidth: 2
  9. }
  10. });
  11. // 绘制第二段弧线 value 从 27 到 48 空出 23到27 之间的位置显示 label
  12. chart.guide().arc({
  13. zIndex: 1,
  14. start: [27, 1.5],
  15. end: [48, 1.5],
  16. style: {
  17. stroke: '#F5F7FB',
  18. lineWidth: 2
  19. }
  20. });
  21. // 绘制第三段弧线
  22. chart.guide().arc({
  23. zIndex: 1,
  24. start: [52, 1.5],
  25. end: [73, 1.5],
  26. style: {
  27. stroke: '#F5F7FB',
  28. lineWidth: 2
  29. }
  30. });
  31. // 绘制第四段弧线
  32. chart.guide().arc({
  33. zIndex: 1,
  34. start: [77, 1.5],
  35. end: [97, 1.5],
  36. style: {
  37. stroke: '#F5F7FB',
  38. lineWidth: 2
  39. }
  40. });

通过上述阴影绘制,加上弧线绘制,再把 label 的 offset 做调整,可以将仪表盘从下图左边的样子变为下图中右边的样子。

G2 定制仪表盘实践 - 图10

绘制色彩分段与渐变

色彩的分段与弧形绘制的原理一致,弧形的分段绘制。这一部分在 G2 的分段仪表盘里也已经有所介绍。不过在这里我仍然想要梳理一下:

为了将仪表盘分为 4 段颜色展示,我们找到3个 value 的分割点,25、50、75。

数据所在区间之前的区间拼接规则如下,顺序不能改变:

当 value >= 25 时,[0, 25] 区间段颜色涂满为 color[0];

当 value >= 50 时,[25, 50] 区间段颜色涂满为 color[1];

当 value >= 75 时,[50, 75] 区间颜色段涂满为 color[2];

数据所在区间涂色规则如下:

当 value < 25 时,[0, value] 区间颜色涂成 color[0];

当 value > 25 && value < 50 时, [25, value] 区间颜色图为 color[1];

当 value > 50 && value < 75 时, [50, value] 区间颜色涂成 color[2];

当 value > 75 时,[75, value] 区间的颜色涂成 color[3];

  1. var color = [
  2. 'l(0) 0:#69B4FA 1:#5AA9FC',
  3. 'l(0) 0:#5AA9FC 1:#546DF6',
  4. 'l(0) 0:#546DF6 1:#5461F7',
  5. 'l(0) 0:#5461F7 1:#474DE2',
  6. ];
  7. var value = data[0].value;
  8. value >= 25 && chart.guide().arc({
  9. zIndex: 1,
  10. start: [0, 0.945],
  11. end: [25, 0.945],
  12. style: {
  13. stroke: color[0],
  14. lineWidth: 18
  15. }
  16. });
  17. value >= 50 && chart.guide().arc({
  18. zIndex: 1,
  19. start: [25, 0.945],
  20. end: [50, 0.945],
  21. style: {
  22. stroke: color[1],
  23. lineWidth: 18
  24. }
  25. });
  26. value >= 75 && chart.guide().arc({
  27. zIndex: 1,
  28. start: [50, 0.945],
  29. end: [75, 0.945],
  30. style: {
  31. stroke: color[2],
  32. lineWidth: 18
  33. }
  34. });
  35. value < 25 && chart.guide().arc({
  36. zIndex: 1,
  37. start: [0, 0.945],
  38. end: [value, 0.945],
  39. style: {
  40. stroke: color[0],
  41. lineWidth: 18
  42. }
  43. });
  44. value < 50 && value > 25 && chart.guide().arc({
  45. zIndex: 1,
  46. start: [25, 0.945],
  47. end: [value, 0.945],
  48. style: {
  49. stroke: color[1],
  50. lineWidth: 18
  51. }
  52. });
  53. value < 75 && value > 50 && chart.guide().arc({
  54. zIndex: 1,
  55. start: [50, 0.945],
  56. end: [value, 0.945],
  57. style: {
  58. stroke: color[2],
  59. lineWidth: 18
  60. }
  61. });
  62. value > 75 && chart.guide().arc({
  63. zIndex: 1,
  64. start: [75, 0.945],
  65. end: [value, 0.945],
  66. style: {
  67. stroke: color[3],
  68. lineWidth: 18
  69. }
  70. });

将指标绘制的部分由上述代码代替分段,其中 color 为4个渐变色组成的数组。即可得到分段渐变的仪表盘了。

G2 定制仪表盘实践 - 图11

小结

其实仪表盘是一个极为简单的图表,其数据一般只有一个,表达这个数据的在其波动区间内的占比。那么自定义它的样式的难点就转化到绘制上,通过上述分析和实践,掌握两个要点就能绘制自己的仪表盘了:仪表盘的圆弧是叠加画出来,指针的形状可以自定义。

完整代码

文章可随意转载,但请保留此 原文链接
非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com