老板:实现一下王者荣耀里的这个数据雷达图,不要用echarts

973 阅读8分钟

小剧场~

办公室的空调嗡嗡作响,我正盯着屏幕敲代码,冷不丁老板抱着平板大步流星走过来,把屏幕怼到我眼前:“小郑,你看王者荣耀里这个雷达图!能把英雄的攻击、防御啥的都整得明明白白,咱网站也得整一个!

我扫了眼屏幕,心里暗自窃喜,这题我会啊!立马拍着胸脯保证:“老板,这简单!用 echarts ,5 分钟准能搞定!到时候数据一填,图表一渲染,保证和游戏里一样炫酷!

话音刚落,就看见老板眉头一皱,嘴角似笑非笑地扯了扯:“不行,不要用 echarts。” 他慢悠悠地推了推眼镜,眼神里闪过一丝 “狡黠”,“老用现成框架多没意思,咱们得整点有技术含量的,用原生代码实现,正好锻炼锻炼你的能力!

我瞬间僵在原地,笑容凝固在脸上。看着老板转身时那意味深长的背影,突然顿悟 —— 合着 5 分钟搞定太轻松,这是怕我 “摸鱼”,特意给我 “加餐” 呢!得,一场和原生代码的 “硬仗”,看来是躲不过去了……

什么是雷达图

在数据可视化的世界里,雷达图占据着很重要的一个地位,无论是分析企业多维度的经营数据,还是评估游戏角色的综合能力,雷达图都能以直观又独特的方式,将复杂信息清晰呈现。

王者荣耀 这个游戏大家都不陌生吧,里面对于玩家能力数据展示用的就是雷达图,这样可以很直观地展示玩家在各个维度的一个能力数据,如下图,你能看出我是一个什么类型的玩家吗?

比赛中也经常会用到雷达图数据来比较两个队伍或者两名选手之间的能力特征和差异,如下图,我们可以很清晰地看出红色方各个维度的能力都是超强的蓝色方被红色方全面包围了,但是其实蓝色方也不是特别的弱,因为蓝色方的各项数据也都是在平均分之上

雷达图实现

方法一:echarts

虽然老板不让我们用echarts来实现,但我们也可以借鉴一下,我们用echarts来快速实现一个试试。

echarts文档

使用echarts,那第一件事肯定是先看文档啦,echarts的文档地址如下:

echarts.apache.org/examples/zh…

echarts示例

在文档中找到我们想要的示例,看两眼就可以开始写代码了

在代码中使用echarts

引入echarts

直接通过cdn引入即可

<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js"></script>
html

准备一个div作为雷达图的容器

<div id="main" style="width: 600px; height: 400px"></div>
javascript
  • 1.初始化echarts实例
const myChart = echarts.init(document.getElementById("main"));
  • 2.指定图表的配置项和数据
const option = {
  radar: {
    indicator: [
      { name: "输出", max: 100 },
      { name: "生存", max: 100 },
      { name: "团战", max: 100 },
      { name: "发育", max: 100 },
      { name: "KDA", max: 100 },
    ],
  },
  series: [
    {
      name: "个人能力雷达图",
      type: "radar",
      data: [
        {
          value: [60, 90, 90, 65, 90],
          name: "个人能力",
          areaStyle: {
            color: "rgba(91, 192, 222, 0.3)",
          },
          lineStyle: {
            color: "rgba(91, 192, 222, 0.3)",
            width: 2,
          },
          symbol: "none",
        },
      ],
    },
  ],
};
  • 3.使用刚指定的配置项和数据显示图表
myChart.setOption(option);

实现效果如下:

方法二:canvas

我们可以看到 echarts 底层其实也是通过 canvas 来实现图表的,那我们也用 canvas 来简单实现一个雷达图试试吧!

html

<div class="container">
  <canvas id="radarChart" width="800" height="800"></canvas>
  <div class="legend"></div>
</div>

这里我们简单一点,图例legend直接使用div元素来实现就可以,canvas作为绘制雷达图的画布。

JavaScrip

这里我们通过类的方式来实现一个RadarChart

构造函数
constructor(canvasId, options) {
  // 获取 Canvas 元素和绘图上下文
  this.canvas = document.getElementById(canvasId);
  this.ctx = this.canvas.getContext("2d");
  // 存储配置信息并计算绘图所需的基本参数
  this.options = options;
  this.centerX = this.canvas.width / 2;
  this.centerY = this.canvas.height / 2;
  this.radius = Math.min(this.centerX, this.centerY) * 0.7;
  // 解析配置中的指标和数据
  this.indicators = this.options.radar.indicator;
  this.series = this.options.series[0];
  this.data = this.series.data;
  this.indicatorCount = this.indicators.length;
  this.angleStep = (Math.PI * 2) / this.indicatorCount;
  // 设置默认颜色方案(用于多组数据时的区分)
  this.defaultColors = [
    "rgba(145, 204, 117, 0.3)",
    "rgba(84, 112, 198, 0.3)",
    "rgba(250, 200, 88, 0.3)",
    "rgba(238, 102, 102, 0.3)",
    "rgba(115, 192, 222, 0.3)",
    "rgba(59, 162, 114, 0.3)",
    "rgba(252, 132, 82, 0.3)",
    "rgba(154, 96, 180, 0.3)",
    "rgba(234, 124, 204, 0.3)",
    "rgba(68, 68, 68, 0.3)",
  ];
  this.defaultLineColors = [
    "rgba(145, 204, 117, 1)",
    "rgba(84, 112, 198, 1)",
    "rgba(250, 200, 88, 1)",
    "rgba(238, 102, 102, 1)",
    "rgba(115, 192, 222, 1)",
    "rgba(59, 162, 114, 1)",
    "rgba(252, 132, 82, 1)",
    "rgba(154, 96, 180, 1)",
    "rgba(234, 124, 204, 1)",
    "rgba(68, 68, 68, 1)",
  ];
  this.init();
}
初始化
init() {
  // 设置起始角度,使第一个指标位于正上方
  this.startAngle = -Math.PI / 2;
  
  // 为每组数据设置默认样式(如果未指定)
  this.data.forEach((item, index) => {
    if (!item.areaStyle) {
      item.areaStyle = { color: this.defaultColors[index % this.defaultColors.length] };
    }
    if (!item.lineStyle) {
      item.lineStyle = { color: this.defaultLineColors[index % this.defaultLineColors.length], width: 2 };
    }
  });
}
  • 作用:设置坐标系方向和处理默认样式
  • 关键点:
    • 使用 -Math.PI/2 作为起始角度(对应 Canvas 的 12 点钟方向)
    • 通过取模运算循环使用预设颜色,支持无限多组数据
绘制网格
drawGrid() {
  // 设置网格线条样式
  this.ctx.strokeStyle = "#ccc";
  this.ctx.lineWidth = 1;
  
  // 绘制 5 层同心多边形网格
  for (let level = 1; level <= 5; level++) {
    const levelRadius = (this.radius / 5) * level;
    this.ctx.beginPath();
    
    // 计算每个顶点的坐标并连接
    for (let i = 0; i < this.indicatorCount; i++) {
      const angle = this.startAngle + i * this.angleStep;
      const x = this.centerX + levelRadius * Math.cos(angle);
      const y = this.centerY + levelRadius * Math.sin(angle);
      
      if (i === 0) this.ctx.moveTo(x, y);
      else this.ctx.lineTo(x, y);
    }
    
    this.ctx.closePath();
    this.ctx.stroke();
  }
  
  // 绘制从中心出发的径向线
  for (let i = 0; i < this.indicatorCount; i++) {
    const angle = this.startAngle + i * this.angleStep;
    const x = this.centerX + this.radius * Math.cos(angle);
    const y = this.centerY + this.radius * Math.sin(angle);
    
    this.ctx.beginPath();
    this.ctx.moveTo(this.centerX, this.centerY);
    this.ctx.lineTo(x, y);
    this.ctx.stroke();
    
    // 绘制指标标签
    const labelOffset = this.radius * 1.1;
    const labelX = this.centerX + labelOffset * Math.cos(angle);
    const labelY = this.centerY + labelOffset * Math.sin(angle);
    this.ctx.fillText(this.indicators[i].name, labelX, labelY);
  }
}
  • 使用两层循环:外层控制网格层数,内层计算多边形顶点
  • 三角函数 Math.cos()Math.sin() 用于计算圆周上的点
  • 标签位置略微向外偏移(radius * 1.1)避免与网格重叠
绘制数据区域
drawData() {
  // 绘制多组数据
  this.data.forEach((item, index) => {
    const { value, areaStyle, lineStyle, lineType } = item;

    // 跳过无效数据
    if (!value || value.length !== this.indicatorCount) return;

    this.ctx.beginPath();

    for (let i = 0; i < this.indicatorCount; i++) {
      const angle = this.startAngle + i * this.angleStep;
      const percent = value[i] / this.indicators[i].max;
      const x = this.centerX + this.radius * percent * Math.cos(angle);
      const y = this.centerY + this.radius * percent * Math.sin(angle);

      if (i === 0) {
        this.ctx.moveTo(x, y);
      } else {
        this.ctx.lineTo(x, y);
      }
    }

    this.ctx.closePath();

    // 填充数据区域(如果有填充颜色)
    if (areaStyle.color && areaStyle.color !== "none") {
      this.ctx.fillStyle = areaStyle.color;
      this.ctx.fill();
    }

    // 设置线条样式
    this.ctx.strokeStyle = lineStyle.color;
    this.ctx.lineWidth = lineStyle.width;

    // 设置线条类型(实线或虚线)
    if (lineType === "dashed" && this.ctx.setLineDash) {
      this.ctx.setLineDash([10, 5]); // 虚线模式:10px实线段 + 5px空白
    } else {
      this.ctx.setLineDash([]); // 实线
    }

    // 绘制数据区域边框
    this.ctx.stroke();

    // 重置线条样式
    this.ctx.setLineDash([]);
  });
}
  • 关键计算
    • percent = value[i] / max:将原始值转换为 0-1 之间的比例
    • radius * percent:根据比例确定数据点到中心的距离
  • 样式处理
    • 使用半透明填充(如 rgba(..., 0.3))使重叠区域可见
    • 每组数据使用不同颜色便于区分
绘制图例
drawLegend() {
  const legendDiv = document.querySelector(".legend");
  
  // 为每组数据创建图例项
  this.data.forEach((item, index) => {
    const { name } = item;
    const { color } = item.areaStyle;
    
    const legendItem = document.createElement("div");
    
    // 创建颜色标识方块
    const colorBox = document.createElement("span");
    colorBox.style.display = "inline-block";
    colorBox.style.width = "10px";
    colorBox.style.height = "10px";
    colorBox.style.backgroundColor = color.replace("0.3", "1"); // 使用不透明颜色
    
    // 创建文本标签
    const legendText = document.createElement("span");
    legendText.textContent = name;
    legendText.style.marginLeft = "5px";
    
    // 组合并添加到图例容器
    legendItem.appendChild(colorBox);
    legendItem.appendChild(legendText);
    legendItem.style.marginBottom = "5px";
    legendDiv.appendChild(legendItem);
  });
}
  • 使用 DOM 操作创建图例元素
  • 将半透明颜色转换为不透明版本(替换 0.3 为 1)
  • 通过 CSS 样式设置图例项的布局
雷达图渲染
render() {
  // 清空画布
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  
  // 按顺序绘制各部分
  this.drawGrid();
  this.drawData();
  this.drawLegend();
}

先绘制网格(底层),再绘制数据(中层),最后绘制图例(HTML 层)

配置信息

配置信息还是和前面使用echarts时的配置信息一样:

const option = {
  radar: {
    indicator: [
      { name: "输出", max: 100 },
      { name: "生存", max: 100 },
      { name: "团战", max: 100 },
      { name: "发育", max: 100 },
      { name: "KDA", max: 100 },
    ],
  },
  series: [
    {
      name: "个人能力雷达图",
      type: "radar",
      data: [
        {
          value: [60, 90, 90, 65, 90],
          name: "个人能力",
          areaStyle: {
            color: "rgba(91, 192, 222, 0.3)",
          },
          lineStyle: {
            color: "rgba(91, 192, 222, 0.3)",
            width: 2,
          },
          symbol: "none",
        },
      ],
    },
  ],
};
// 创建并渲染雷达图
const radarChart = new RadarChart("radarChart", option);
radarChart.render();
雷达图效果


试下多组数据的效果

配置信息调整
const option = {
  radar: {
    indicator: [
      { name: "KDA", max: 100 },
      { name: "红方胜率", max: 100 },
      { name: "场均推塔差", max: 100 },
      { name: "场均暴君", max: 100 },
      { name: "场均经济差", max: 100 },
      { name: "蓝方胜率", max: 100 },
    ],
  },
  series: [
    {
      name: "数据对比",
      type: "radar",
      data: [
        {
          value: [100, 100, 100, 95, 100, 100],
          name: "红色方",
          areaStyle: {
            color: "rgba(143,44,35, 0.3)",
          },
          lineStyle: {
            color: "rgba(143,44,35, 0.5)",
            width: 2,
          },
          lineType: "solid",
        },
        {
          value: [68, 85, 68, 70, 90, 63],
          name: "蓝色方",
          areaStyle: {
            color: "rgba(91, 192, 222, 0.3)",
          },
          lineStyle: {
            color: "rgba(91, 192, 222, 0.5)",
            width: 2,
          },
          lineType: "solid",
        },
        {
          value: [50, 55, 52, 53, 52, 52],
          name: "平均值",
          areaStyle: {
            color: "none",
          },
          lineStyle: {
            color: "rgba(255, 255, 255, 0.5)", // 修改为可见颜色
            width: 3,
          },
          lineType: "dashed", // 指定为虚线
        },
      ],
    },
  ],
};
雷达图效果

源码地址

gitee

gitee.com/zheng_yongt…


  • 🌟 觉得有帮助的可以点个 star~
  • 🖊 有什么问题或错误可以指出,欢迎 pr~
  • 📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。