自定义vue项目的雷达图组件
主要思路:
利用canvas,通过传入一组数组数据,根据数组数据的个数,自动生成一个多边形的雷达图形,并在对应的坐标点绘制。
还有一个难点,就是需要计算原点是否在有数值的几个点连成的图形中,如果在图形中,则不连接原点,如果是在图形外,则要连接原点的坐标形成新的图形。
这里判断原点是否在图形中,用的基本思想是利用射线法,以被测点Q为端点,向任意方向作射线(一般水平向右作射线),计算射线与多边形各边的交点,如果是偶数,则点在多边形外,否则在多边形内。还会考虑一些特殊情况,如点在多边形顶点上,点在多边形边上等特殊情况。
判断算法描述如下:首先,对于多边形的水平边不作考虑;其次,对于多边形的顶点和射线相交的情况,如果该顶点是其所属的边上纵坐标较大的顶点,则计数,否则忽略该点;最后,对于Q在多边形边上的情形,直接可以判断Q属于多边形
参考:http://paulbourke.net/geometry/insidepoly/
目前可传入的配置参数,后续可根据项目的需求自由添加实现新的配置参数:
areaColor: ’rgba(140,144,220,0.55) // 雷达数据坐标点围起来的图形的填充颜色
segmentLineColor: '#d8d8d8' // 原点到数据坐标点的连线的颜色
diagonalLineColor: 'rgba(216,216,216,0.4)' // 雷达对角线的颜色
numberLineColor: ‘rgba(140,144,220,0.3)’ // 雷达线的颜色
axisTextColor: ‘rgba(140,144,220,0.8)’ // 数值文字的颜色
edgeNumber: 4 // 分割线的数量
textSize: 16 // 文字的尺寸大小
textSpace: 12 // 文字的间距大小
polygons: [ 0.1, 0.2 ] // 雷达坐标数值的数组(小数0-1)
texts: [ '1', '2' ] // 雷达坐标标题的数组
fontSize: 14 // 字体字号大小
pointColor: '#d5d6f0' // 小圆点颜色
showNumber: true // 显示数值
组件代码:
polygon-custom.vue
<template>
<div class="polygon-container">
<canvas ref="polygon" id='polygon' class='polygon' width="100" height="100"></canvas>
</div>
</template>
<style lang="less" scoped>
.polygon-container {
.polygon {
display: block;
width: 3rem;
height: 3rem;
margin: 0 auto;
}
}
</style>
<script>
let context = null;
let canvasWidth = 0;
let canvasHeight = 0;
let TEXT_SPACE = 12;
export default {
/**
* 组件的属性列表
*/
props: {
areaColor: { // 雷达数据坐标点围起来的图形的填充颜色
type: String,
default: "rgba(140,144,220,0.55)",
},
segmentLineColor: { // 原点到数据坐标点的连线的颜色
type: String,
default: "#d8d8d8",
},
diagonalLineColor: { // 雷达对角线的颜色
type: String,
default: "rgba(216,216,216,0.4)",
},
numberLineColor: { // 雷达线的颜色
type: String,
default: "rgba(140,144,220,0.3)",
},
axisTextColor: { // 数值文字的颜色
type: String,
default: "rgba(140,144,220,0.8)",
},
edgeNumber: { // 分割线的数量
type: Number,
default: 4,
},
textSize: { // 文字的尺寸大小
type: Number,
default: 16,
},
textSpace: { // 文字的间距大小
type: Number,
default: TEXT_SPACE,
},
polygons: { // 雷达坐标数值的数组(小数0-1)
type: Array,
default: () => [],
},
texts: { // 雷达坐标标题的数组
type: Array,
default: () => [],
},
fontSize: { // 字体字号大小
type: Number,
default: 14
},
pointColor: { // 小圆点颜色
type: String,
default: '#d5d6f0'
},
showNumber: { // 显示数值
type: Boolean,
default: true
}
},
/**
* 组件的初始数据
*/
data: {},
mounted () {
this.$nextTick(() => {
setTimeout(() => {
const canvas = this.$refs['polygon'];
context = canvas.getContext('2d');
canvas.width = canvas.offsetWidth
canvas.height = canvas.offsetHeight
canvasWidth = canvas.offsetWidth;
canvasHeight = canvas.offsetHeight;
this.run();
}, 600)
})
},
/**
* 组件的方法列表
*/
methods: {
run() {
if (this.polygons.length < 3) {
return;
}
if (this.texts.length < this.polygons.length) {
for (let i = this.polygons.length; i >= this.texts.length; i-- ) {
this.texts.push("空");
}
}
var center_x = canvasWidth / 2;
var center_y = canvasHeight / 2;
var radius = ((canvasWidth > canvasHeight ? canvasHeight : canvasWidth) - 2 * this.textSpace - this.textSize * 2) / 2;
var innerAngle = 360 / this.polygons.length;
this.drawSegmentLine(context, center_x, center_y, radius, innerAngle);
this.drawDiagonalLine(context, center_x, center_y, radius, innerAngle);
this.drawNumberLine(context, center_x, center_y, radius, innerAngle);
this.drawAxisText(context, center_x, center_y, radius, innerAngle);
this.drawShadowArea(context, center_x, center_y, radius, innerAngle);
},
// 画雷达图
drawShadowArea(context, center_x, center_y, radius, innerAngle) {
context.fillStyle = this.areaColor;
context.strokeStyle = this.segmentLineColor;
context.lineWidth = 1;
context.beginPath();
let polygon = []
for (let i = 0; i < this.polygons.length; i++) {
var f = this.polygons[i];
if (f > 1) {
f = 1;
}
if (f < 0) {
f = 0;
}
var currentRadius = radius * f;
var x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * i)) * currentRadius;
var y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * i)) * currentRadius;
if (currentRadius !== 0) {
polygon.push({
x, y
})
context.lineTo(x, y);
}
if(i == this.polygons.length - 1) {
let inP = this.isPointInPolygon({x: center_x, y: center_y },polygon)
console.log('inP',inP)
if(!inP) {
context.lineTo(center_x, center_y);
}
}
}
context.closePath();
context.fill();
},
// 画分割线
drawSegmentLine(context, center_x, center_y, radius, innerAngle) {
context.strokeStyle = this.segmentLineColor;
for (let i = 0; i <= this.edgeNumber; i++) {
context.lineWidth = 1;
context.beginPath();
for (let j = 0; j < this.polygons.length; j++) {
var x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * ((radius * i) / this.edgeNumber);
var y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * ((radius * i) / this.edgeNumber);
context.lineTo(x, y);
}
context.closePath();
context.stroke();
}
},
// 画雷达数据线,即原点到数据点的连线
drawNumberLine(context, center_x, center_y, radius, innerAngle) {
for (let j = 0; j < this.polygons.length; j++) {
context.strokeStyle = this.numberLineColor;
if (this.polygons[j]) {
let temp_x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * this.polygons[j] * radius;
let temp_y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * this.polygons[j] * radius;
context.beginPath();
context.moveTo(center_x, center_y);
context.lineTo(temp_x, temp_y);
context.closePath();
context.stroke();
// 画小圆形
context.strokeStyle = this.pointColor;
context.beginPath();
context.arc(temp_x, temp_y, 2, 0, 2 * Math.PI);
context.closePath();
context.fill();
if(this.showNumber) {
// 写数值
var text_size = 10
var text_x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * (this.polygons[j] * radius + text_size / 2);
var text_y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * (this.polygons[j] * radius + this.textSize / 2);
context.beginPath();
context.font = text_size+'px'; // 字体大小 注意不要加引号
context.fillStyle = this.axisTextColor; // 字体颜色
context.textAlign = "center"; // 字体位置
context.textBaseline = "middle"; // 字体对齐方式
context.fillText(Math.floor(this.polygons[j]*100)+"%", text_x, text_y); // 文字内容和文字坐标
context.closePath();
}
}
}
},
// 画对角线
drawDiagonalLine(context, center_x, center_y, radius, innerAngle) {
context.strokeStyle = this.diagonalLineColor;
context.lineWidth = 0.5;
for (let j = 0; j < this.polygons.length; j++) {
context.setLineDash([2, 6]);
context.beginPath();
context.lineTo(center_x, center_y);
var x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * radius;
var y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * radius;
context.lineTo(x, y);
context.closePath();
context.stroke();
}
context.setLineDash([]);
},
// 写文字
drawAxisText(context, center_x, center_y, radius, innerAngle) {
for (let j = 0; j < this.texts.length; j++) {
let text = this.texts[j];
context.lineTo(center_x, center_y);
var x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * (radius + this.textSpace + this.textSize / 2);
var y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * (radius + this.textSpace + this.textSize / 2);
context.beginPath();
context.font = this.fontSize + 'px'; // 字体大小 注意不要加引号
context.fillStyle = this.axisTextColor; // 字体颜色
context.textAlign = "center"; // 字体位置
context.textBaseline = "middle"; // 字体对齐方式
context.fillText(text, x, y); // 文字内容和文字坐标
}
},
angleToRadian(angle) {
return ((2 * Math.PI) / 360) * angle;
},
// 判断点是否在平面中
isPointInPolygon(point, polygon) {
// 下述代码来源:http://paulbourke.net/geometry/insidepoly/,进行了部分修改
// 基本思想是利用射线法,计算射线与多边形各边的交点,如果是偶数,则点在多边形外,否则在多边形内。还会考虑一些特殊情况,如点在多边形顶点上,点在多边形边上等特殊情况。
var N = polygon.length;
var boundOrVertex = true; // 如果点位于多边形的顶点或边上,也算做点在多边形内,直接返回true
var intersectCount = 0; // cross points count of x
var precision = 2e-10; // 浮点类型计算时候与0比较时候的容差
var p1, p2; // neighbour bound vertices
var p = point; // 测试点
p1 = polygon[0]; //left vertex
for (var i = 1; i <= N; ++i) { //check all rays
if (p.x == p1.x && p.y == p1.y) {
return boundOrVertex; //p is an vertex
}
p2 = polygon[i % N]; //right vertex
if (p.y < Math.min(p1.y, p2.y) || p.y > Math.max(p1.y, p2.y)) { //ray is outside of our interests
p1 = p2;
continue; //next ray left point
}
if (p.y > Math.min(p1.y, p2.y) && p.y < Math.max(p1.y, p2.y)) { //ray is crossing over by the algorithm (common part of)
if (p.x <= Math.max(p1.x, p2.x)) { //x is before of ray
if (p1.y == p2.y && p.x >= Math.min(p1.x, p2.x)) { //overlies on a horizontal ray
return boundOrVertex;
}
if (p1.x == p2.x) { //ray is vertical
if (p1.x == p.x) { //overlies on a vertical ray
return boundOrVertex;
} else { //before ray
++intersectCount;
}
} else { //cross point on the left side
var xinters = (p.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x; // x轴方向上射线与p1,p2线段的交点的x坐标
// 判断p点的x坐标是否与交点的x坐标重合,允许有误差值precision
if (Math.abs(p.x - xinters) < precision) {
return boundOrVertex;
}
if (p.x < xinters) { //before ray
++intersectCount;
}
}
}
} else { // special case when ray is crossing through the vertex
if (p.y == p2.y && p.x <= p2.x) { //p crossing over p2
var p3 = polygon[(i + 1) % N]; //next vertex
if (p.y >= Math.min(p1.y, p3.y) && p.y <= Math.max(p1.y, p3.y)) { //p.y lies between p1.y & p3.y
++intersectCount;
} else {
intersectCount += 2;
}
}
}
p1 = p2; //next ray left point
}
if (intersectCount % 2 == 0) { // 偶数在多边形外
return false;
} else { // 奇数在多边形内
return true;
}
}
},
}
</script>
组件调用:
<template>
<PolygonCustom class='polygon' :polygons='polygons' :texts='texts' :fontSize="10" :areaColor="'rgba(90, 205, 199, 0.35)'" :segmentLineColor="'rgba(90, 205, 199, 0.37)'" :axisTextColor="'#39D1CA'" :edgeNumber="10" :textSize="30" :textSpace="1" :pointColor="'#5ACDC7'" :showNumber="false"></PolygonCustom>
</template>
<script>
import PolygonCustom from 'xxx'
export default {
data() {
retutn {
polygons: [], // [0.85, 0, 0.65, 0, 0.8, 0.9, 1, 0.7],
texts: [], // ['湿热质', '气虚质', '气郁质', '平和质', '痰湿质', '气郁质', '平和质', '痰湿质'],
}
},
components: {
PolygonCustom
}
}
</script>