Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

通过少量切图实现雷达图的一种方式(不用SVG和Canvas) #57

Open
lihongxun945 opened this issue May 12, 2023 · 0 comments

Comments

@lihongxun945
Copy link
Owner

lihongxun945 commented May 12, 2023

背景

1
最近需要做一个雷达图如上图所示。雷达图有6个维度,每一个维度都有3种可能得数值(3星、4星、5星)。

实现方案

比较坑的是要在weex 1.0 环境中绘制雷达图,因此无法使用svg或canvas来画图。可行方案是离线生成所有图片,前端直接根据数据加载对应的图片。如果是每一种数值都生成一张图片,全部加起来就是 3^6=729 个图片,显然这个工作量太大了,而且非常容易出错。那有没有方式能够减少切图数量呢?

2

首先可以把这个雷达图拆分成三个图层,如上图所示,其中背景图显然是固定的,重要的是如何画出六边形和序号。

拆分实现六边形

3

因为六边形的所有情况有729种,枚举难度太大,所以需要换一个思路。注意到雷达图其实是正六边形,可以拆分成6个不同的等边三角形,这样每一个三角形都只有 3x3=9 种情况,也就是只需要9个切图就好了,比729少了很多。

每一个扇区只需要根据两个顶点的值选择9个切图中的一个,然后进行旋转即可,代码实现也并不复杂,核心逻辑如下:

// 九个切图
const PIES = {
  '33': 'https://gw.alicdn.com/imgextra/i3/O1CN01blqPsA28X1zr5d78v_!!6000000007941-2-tps-240-278.png',
  '34': 'https://gw.alicdn.com/imgextra/i4/O1CN01eALo701waRYewfwwW_!!6000000006324-2-tps-240-278.png',
  '35': 'https://gw.alicdn.com/imgextra/i4/O1CN0139AIMs27sj8qvgyad_!!6000000007853-2-tps-240-278.png',
  '43': 'https://gw.alicdn.com/imgextra/i1/O1CN01fB2Rm91GS0OqD6o4u_!!6000000000620-2-tps-240-278.png',
  '44': 'https://gw.alicdn.com/imgextra/i3/O1CN01TgKL8j1mjTCqpYrLU_!!6000000004990-2-tps-240-278.png',
  '45': 'https://gw.alicdn.com/imgextra/i1/O1CN01DiB6uC1uWCev00b2G_!!6000000006044-2-tps-240-278.png',
  '53': 'https://gw.alicdn.com/imgextra/i4/O1CN01MGnlRI1Cl0MkFLMMI_!!6000000000120-2-tps-240-278.png',
  '54': 'https://gw.alicdn.com/imgextra/i4/O1CN01U1JxHt1TB8oyPdWM5_!!6000000002343-2-tps-240-278.png',
  '55': 'https://gw.alicdn.com/imgextra/i2/O1CN01cmll6b1M45PL3rcnD_!!6000000001380-2-tps-240-278.png',
};

// 渲染切图
scores.map((score, index) => {
  const pie = PIES[score + '' + (index === 5 ? scores[0] : scores[index + 1])];
  const deg = 360 / 6 * index;
  const style = {
    transform: `rotate(${deg}deg)`,
  };
  return (
    <Picture source={{ uri: pie }} className="radar-pie" style={style} key={index} />
  );
})

计算序号位置

6个序号的位置也并不是固定的,需要根据分数定位到对应的六边形顶点。为了实现这个定位,需要计算相对于左上角的水平和垂直距离,这里只需要一点点的初中三角函数知识就可以做到。以右上角的序号为例:
4

其相对于左上角的坐标为:

  • x=width/2+width/2*score/max
  • y=height/2-()width/2*score/max*cotan(30°)

完整的六个顶点的计算公式如下:

// 注意,设计稿并不是严格按照五等分计算的
const percent = function (score) {
  return [0, 0, 0, 0.333, 0.667, 1][score];
};
const TANH30 = 1 / Math.sqrt(3); // cotan 30 deg
const points = [
  [WIDTH / 2, HEIGHT / 2 * (1 - percent(scores[0]))],
  [WIDTH / 2 * (1 - percent(scores[1])), HEIGHT / 2 - (WIDTH / 2 * percent(scores[1]) * TANH30)],
  [WIDTH / 2 * (1 - percent(scores[2])), HEIGHT / 2 + (WIDTH / 2 * percent(scores[2]) * TANH30)],
  [WIDTH / 2, HEIGHT / 2 + HEIGHT / 2 * percent(scores[3])],
  [WIDTH / 2 + (WIDTH / 2 * percent(scores[4])), HEIGHT / 2 + (WIDTH / 2 * percent(scores[4]) * TANH30)],
  [WIDTH / 2 + (WIDTH / 2 * percent(scores[5])), HEIGHT / 2 - (WIDTH / 2 * percent(scores[5]) * TANH30)],
];

总结
这个方案好处是可以不用SVG和canvas,只需要少量的切图就能实现雷达图,有较好的兼容性。
缺点是数值范围较大的情况下,切图可能会变得很多,切图数量是 (range)^2 ,如果有10种取值,那么就有 10^2=100个切图,如果取值范围不可枚举(比如图中要精确到小数显示比例),就无法用这种方式实现。

5

手机上的实际渲染表现
另外我也实现了SVG的版本,其实原理是一样的,而且不需要切图,只需要计算六个顶点的坐标即可,代码更加简洁,核心代码:

const points = [
  [WIDTH / 2, HEIGHT / 2 * (1 - scores[0] / MAX)],
  [WIDTH / 2 * (1 - scores[1] / MAX), HEIGHT / 2 - (WIDTH / 2 * (scores[1] / MAX) * TANH30)],
  [WIDTH / 2 * (1 - scores[2] / MAX), HEIGHT / 2 + (WIDTH / 2 * (scores[2] / MAX) * TANH30)],
  [WIDTH / 2, HEIGHT / 2 + HEIGHT / 2 * (scores[3] / MAX)],
  [WIDTH / 2 + (WIDTH / 2 * (scores[4] / MAX)), HEIGHT / 2 + (WIDTH / 2 * (scores[4] / MAX) * TANH30)],
  [WIDTH / 2 + (WIDTH / 2 * (scores[5] / MAX)), HEIGHT / 2 - (WIDTH / 2 * (scores[5] / MAX) * TANH30)],
];

const d = `M${points.join('L')}L${points[0]}`;

return (
<svg width={WIDTH} height={HEIGHT} viewBox={`-${LINE_WIDTH / 2} -${LINE_WIDTH / 2} ${WIDTH + LINE_WIDTH} ${HEIGHT + LINE_WIDTH}`} xmlns="https://www.w3.org/2000/svg">
  <g>
  <path
    fill="rgba(92,173,255,.5)"
    stroke="#2B94FF"
    stroke-width={LINE_WIDTH}
    d={d}
  />
  </g>
</svg>
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant