可视化工具成为了数据分析不可或缺的一部分,雷达图(又称蜘蛛网图)作为一种多维数据可视化工具,能够直观地展示多个变量的对比情况。本文介绍如何使用Vue.js和D3.js结合,创建一个具有交互功能的雷达图组件。
总结一篇超级详细的d3 雷达图配置,每行基本带上了备注,直接有手就会的。记得点点赞👍
1、效果图预览:
2、项目背景与需求
假设有一个数据为(demo 默认数据)安全评估系统,需要对服务器安全、数据库安全、网络防护、应用防护和数据加密等多个维度进行评分。希望将这些评分以雷达图的形式展示出来,并允许用户通过鼠标悬停查看具体评分,同时支持缩放功能以便更细致地观察数据。
3、准备工作
首先,确保项目中已经安装了 d3.js如果还没有安装,可以通过 npm 或 yarn 进行安装:
npm install vue d3
或
yarn add vue d3
4、雷达图绘制前了解基本构成概述:
- 网格(Grid) :绘制若干圆形网格,表示不同的数值范围。
- 坐标轴(Axes) :每个坐标轴代表一个维度,通常是数据集中的特定特征。
- 数据点(Data Points) :每个数据点对应一个维度上的值,通常通过圆形或其他标记来表示。
- 工具提示(Tooltip) :用于在鼠标悬停时显示数据点的具体信息。
- 缩放(Zoom) :允许用户缩放图表,查看不同的细节。
5、雷达图组件实现
1. 组件结构
首先,创建一个名为RadarChart的Vue组件,创建模板部分。
<template>
<div ref="radarContainer" style="width: 100%; height: 100%;"></div>
</template>
2. 数据处理与监听
在组件的props中,定义了data、fieldValue和unit三个属性,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…
这里还是存在一个问题,就是绘制雷达图区域(数据的多边形区域和 绘制数据点没有在同一点位上),有会的大佬,记得在评论或者私信@我一下,让我再完善!!!