使用D3.js绘制雷达图

584 阅读5分钟

可视化工具成为了数据分析不可或缺的一部分,雷达图(又称蜘蛛网图)作为一种多维数据可视化工具,能够直观地展示多个变量的对比情况。本文介绍如何使用Vue.js和D3.js结合,创建一个具有交互功能的雷达图组件。

总结一篇超级详细的d3 雷达图配置,每行基本带上了备注,直接有手就会的。记得点点赞👍

1、效果图预览:

12.gif

2、项目背景与需求

假设有一个数据为(demo 默认数据)安全评估系统,需要对服务器安全、数据库安全、网络防护、应用防护和数据加密等多个维度进行评分。希望将这些评分以雷达图的形式展示出来,并允许用户通过鼠标悬停查看具体评分,同时支持缩放功能以便更细致地观察数据。

3、准备工作

首先,确保项目中已经安装了 d3.js如果还没有安装,可以通过 npm 或 yarn 进行安装:

npm install vue d3

yarn add vue d3

4、雷达图绘制前了解基本构成概述:

  1. 网格(Grid) :绘制若干圆形网格,表示不同的数值范围。
  2. 坐标轴(Axes) :每个坐标轴代表一个维度,通常是数据集中的特定特征。
  3. 数据点(Data Points) :每个数据点对应一个维度上的值,通常通过圆形或其他标记来表示。
  4. 工具提示(Tooltip) :用于在鼠标悬停时显示数据点的具体信息。
  5. 缩放(Zoom) :允许用户缩放图表,查看不同的细节。

5、雷达图组件实现

1. 组件结构

首先,创建一个名为RadarChart的Vue组件,创建模板部分。

<template>
  <div ref="radarContainer" style="width: 100%; height: 100%;"></div>
</template>

2. 数据处理与监听

在组件的props中,定义了datafieldValueunit三个属性,data是包含评分信息的数组,fieldValue指定了要展示的字段值,unit用于拼接单位。

通过watch属性,监听data的变化,并在数据更新时重新渲染雷达图。

props: {
  // ...
},
watch: {
  data: {
    handler(newVal) {
      newVal && this.createRadarChart(newVal);
    },
    deep: true
  }
}

3. 生命周期钩子

mounted钩子中,确保在DOM元素挂载后初始化雷达图,并添加窗口大小变化的监听器。在beforeDestroy钩子中,移除该监听器以防止内存泄漏。

mounted() {
  this.$nextTick(() => {
    this.data && this.createRadarChart(this.data);
    window.addEventListener('resize', this.handleResize);
  });
},
beforeDestroy() {
  window.removeEventListener('resize', this.handleResize);
}

4. 雷达图绘制及逐步解析

  1. 初始化图形的容器和参数

根据容器的宽度和高度来设置雷达图的尺寸,通过 Math.min(width, height) 来确保雷达图是正圆形,并为它留出一定的边距:

const width = this.$refs.radarContainer.offsetWidth; // 获取容器的宽度
const height = this.$refs.radarContainer.offsetHeight; // 获取容器的高度
const margin = 30;
const radius = Math.min(width, height) / 2 - margin; // 雷达图的半径
const angleSlice = Math.PI * 2 / data.length; // 每个维度的角度
const color = d3.scaleOrdinal(d3.schemeCategory10); // 调色板
2. 构造雷达图的核心数据结构

根据传入的数据 data 构造了一个新的数组 radarData,这个数组将包含每个维度的名称 (axis)、数值 (value),以及它在雷达图上的角度位置 (angle)。

const radarData = data.map((d, i) => ({
  axis: d.name,
  value: d[this.fieldValue],
  angle: angleSlice * i,
}));
3.绘制雷达图的网格

雷达图的网格由一系列同心圆组成,下面的代码生成了 5 个同心圆,并根据数据的比例来调整圆的半径,d3.range(1, 6) 生成一个从 1 到 5 的数组,这里代表不同的网格层级,radius / 5 * d 用来计算每个圆的半径

const grid = g.append('g').attr('class', 'grid');
grid.selectAll('.grid-circle')
  .data(d3.range(1, 6))
  .enter()
  .append('circle')
  .attr('class', 'grid-circle')
  .attr('r', (d) => radius / 5 * d)
  .attr('fill', 'none')
  .attr('stroke', '#ccc')
  .attr('stroke-width', 1);
4. 绘制每个维度的轴

每个维度都有一条从图表中心到边缘的线,表示该维度的轴。通过 Math.cos() 和 Math.sin() 来计算每个轴的终点坐标,从而将轴线从中心点伸展到雷达图的边缘。

const axis = g.selectAll('.axis')
  .data(radarData)
  .enter()
  .append('line')
  .attr('class', 'axis')
  .attr('x1', 0)
  .attr('y1', 0)
  .attr('x2', (d) => radius * Math.cos(d.angle))
  .attr('y2', (d) => radius * Math.sin(d.angle))
  .attr('stroke', '#ccc')
  .attr('stroke-width', 1);
5. 绘制轴标签

每个维度的名称会显示在相应轴的末端。通过计算每个轴的角度并将文本放置在轴的末端位置,来显示维度标签。

g.selectAll('.axis-label')
  .data(radarData)
  .enter()
  .append('text')
  .attr('class', 'axis-label')
  .attr('x', (d) => (radius + 10) * Math.cos(d.angle))
  .attr('y', (d) => (radius + 10) * Math.sin(d.angle))
  .text((d) => d.axis)
  .attr('text-anchor', 'middle')
  .attr('font-size', '10px')
  .attr('fill', '#666');

------------- 更新---------
当数据过多时,标签会出现重叠,这里适当调整标签的方向

// 绘制每个维度的标签
      g.selectAll('.axis-label')
        .data(radarData)
        .enter()
        .append('text')
        .attr('class', 'axis-label')
        .attr('x', (d) => (radius + 10) * Math.cos(d.angle))  // 标签位置
        .attr('y', (d) => (radius + 10) * Math.sin(d.angle))
        .text((d) => d.axis)  // 显示轴的名称
        .attr('text-anchor', 'middle')
        .attr('font-size', '10px')
        .attr('fill', '#666')
        .attr('transform', (d) => {
          const angle = d.angle * 180 / Math.PI;  // 转换为度
          // 角度大于 180°时,标签反向旋转
          const rotationAngle = angle > 90 && angle < 270 ? angle + 180 : angle;  // 如果在后半部分,反向旋转
           // // 旋转标签,使其适应角度
          return `rotate(${rotationAngle}, ${(radius + 10) * Math.cos(d.angle)}, ${(radius + 10) * Math.sin(d.angle)})`;
        });
6. 绘制数据点

每个数据点对应雷达图中的一个圆点,圆点的位置由维度值(d.value)和角度(d.angle)决定。通过 Math.cos() 和 Math.sin() 来计算圆心的坐标。

const points = g.selectAll('.radar-point')
  .data(radarData)
  .enter()
  .append('circle')
  .attr('class', 'radar-point')
  .attr('cx', (d) => d.value * Math.cos(d.angle) * (radius / 100))  // 计算每个点的x坐标,基于角度和数值
  .attr('cy', (d) => d.value * Math.sin(d.angle) * (radius / 100))  // 计算每个点的y坐标,基于角度和数值
  .attr('r', 4)
  .attr('fill', '#2872df') 
  .attr('stroke', '#fff')
  .attr('stroke-width', 1);
7. 添加交互效果:鼠标悬停提示 

当鼠标悬停在数据点上时,显示 tooltip信息板,展示该数据点的维度名称和数值。通过 mouseover 和 mouseout 事件来控制 tooltip 的显示与隐藏。

const tooltip = svg.append('g')
  .attr('class', 'tooltip')
  .style('display', 'none');
tooltip.append('rect')
  .attr('x', 10)
  .attr('y', 10)
  .attr('rx', 5)
  .attr('ry', 5)
  .attr('width', 120)
  .attr('height', 40)
  .attr('fill', 'rgba(0, 0, 0, 0.7)');
tooltip.append('text')
  .attr('x', 20)
  .attr('y', 30)
  .attr('fill', '#fff')
  .attr('font-size', '12px'); 
points.on('mouseover', function (event, d) {
  tooltip.style('display', null);
  tooltip.select('text')
    .text(`${d.axis}: ${d.value}`);
  tooltip.attr('transform', `translate(${event.offsetX + 10}, ${event.offsetY - 30})`);
})
.on('mouseout', function () {
  tooltip.style('display', 'none');
});

提示版这里需要微调,主要数width固定了宽度,导致数据名称过长时背景显示不全(补充)

当鼠标移入时,获取文本的宽度和高度,动态调整tooltip的width属性的宽度

 // 鼠标悬停时显示tooltip
 points.on('mouseover', function (event, d) {
      tooltip.style('display', null);  // 显示tooltip
      // 更新文本内容
      const text = `${d.axis}: ${d.value}${d.unit}`;
      tooltip.select('text').text(text); // 显示维度名称和对应的值
      tooltip.attr('fill', '#fff'); // 设置文本颜色为白色
 
      // 获取文本的宽度
      const textWidth = tooltip.select('text').node().getBBox().width;
      const textHeight = tooltip.select('text').node().getBBox().height;
      // 动态设置矩形的宽度和高度
      tooltip.select('rect')
          .attr('width', textWidth + 20)  // 矩形宽度根据文本宽度动态调整
          .attr('height', textHeight + 20);  // 矩形高度根据文本高度动态调整
 
      tooltip.attr('transform', `translate(${event.offsetX + 10}, ${event.offsetY - 30})`);  // 设置tooltip的位置
 
      })
        .on('mouseout', function () {
          tooltip.style('display', 'none');  // 隐藏tooltip
        });
8. 添加缩放功能 

通过 d3.zoom() 来为雷达图添加缩放功能。用户可以使用鼠标滚轮或拖动来缩放雷达图,图形会根据用户的缩放操作进行适当的缩放和平移。

const zoom = d3.zoom()
  .scaleExtent([0.5, 2])
  .on('zoom', function (event) {
    g.attr('transform', `translate(${width / 2},${height / 2})` + event.transform);
  });
svg.call(zoom);

9.关于数据的多边形区域和 绘制数据点没有在同一点位上的补充

const radarDataWithRotation = radarData.map((d, i) => ({
        ...d,  // 保留原始数据
        angle: d.angle + angleOffset  // 为每个点的角度加上偏移量
      }));
// 确保路径闭合,将第一个点加到最后
const closedRadarData = [...radarDataWithRotation, radarDataWithRotation[0]];  // 将第一个点再次加到末尾
// 绘制雷达图区域(数据的多边形区域)
const radarLine = d3.lineRadial()
     .radius((d) => d.value * (radius / 100))  // 根据每个维度的值来确定每个点的半径
     .angle((d) => d.angle);  // 根据角度来确定每个点的位置
 
g.append('path')
 .datum(closedRadarData)  // 绑定数据
 .attr('class', 'radar-line')
 .attr('d', radarLine)  // 绘制路径
 .attr('fill', 'rgba(40, 114, 223, 0.3)')  // 填充颜色(半透明蓝色)
 .attr('stroke', '#2872df')  // 边框颜色
 .attr('stroke-width', 2);  // 边框宽度

5. 样式与交互

<style scoped>
.tooltip rect {
  opacity: 0.7;
}
</style>

预留字段:

fieldValue‌: 指定数据源中需要展示的字段值,方便在数据格式变化时,无需修改组件代码即可适应新的数据结构。

unit‌:预留的单位属性,可用于在数据值后添加单位(如'%'、'个'等),增强图表的可读性。目前暂未使用,但为未来使用时提供便利。

总结

本文展示如何使用 D3.js 绘制一个交互式的雷达图,包括网格、轴、数据点、交互式提示和缩放功能的实现。通过这些功能,可以更灵活地展示多维数据并为用户提供丰富的交互体验。可以根据自己的需求进一步扩展功能或调整样式,使雷达图更符合自己实际应用场景的要求。

整体代码:download.csdn.net/download/zh…

这里还是存在一个问题,就是绘制雷达图区域(数据的多边形区域和 绘制数据点没有在同一点位上),有会的大佬,记得在评论或者私信@我一下,让我再完善!!!