利用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>
- 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 = []
- let jgNum = 0, jgNumFlag = false // 前一个数值跟第二个数值的间隔
-
- 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
- })
-
-
- // 防止间隔过大,导致连线的图形交叉
- if(jgNum > this.polygons.length/2) {
- jgNum = 0;
- jgNumFlag = true
- context.lineTo(center_x, center_y);
- }
- context.lineTo(x, y);
- } else {
- jgNum++
- }
-
- if(i == this.polygons.length - 1) {
-
- let inP = this.isPointInPolygon({x: center_x, y: center_y },polygon)
- console.log('inP',inP)
- if(!inP && !jgNumFlag) {
- 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
-
- // p是顶点
- if (p.x == p1.x && p.y == p1.y) {
- return boundOrVertex; //p is an vertex
- }
-
- p2 = polygon[i % N]; //right vertex
-
- // p射线不相交,直接跳过
- 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
- }
-
-
- // p射线跟线段相交
- 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
- // p射线跟顶点相切
-
- 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;
- }
- }
- },
- }
组件调用:
- <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>