用数学思维实现雷达分析图
来源:互联网 发布:校园网解绑网卡mac地址 编辑:程序博客网 时间:2024/04/29 17:24
前言
前段时间回看里约奥运会的国球比赛,岛国媒体给我龙队一个响亮的称号—— 六边形战士 !
马龙是我的偶像,看到这样的称号当然很骄傲。
分析图片可以知道:六个定点分别标识个技术点名称,对应 半径 所填充长度表示分值,龙队在各方面的分数都是满分,所以在雷达分析图上覆盖区全部填充。
作为程序员的我,不免要从技术实现的角度思考问题,接下来我们一起造轮子:
先上效果图:
设计思路
回顾两个知识点:
- 在一平面中,确定一坐标原点(0,0),水平向右为x轴正方向,竖直向上为y轴正方向,从右上方开始顺时针依次为第一象限、第二象限、第三象限和第四象限。
- 平面中的任一点的坐标应该是:其与原点所在直线的倾斜角的余弦为x,正弦值为y。
从效果图来看,我们应该把view区域按照数学中的平面坐标来区分,雷达图中心点(外接圆圆心)为坐标原点,水平向右的半径为x轴正方向,竖直向上的半径为y轴正方向,从右上方开始顺时针依次为第一象限、第二象限、第三象限和第四象限。
大致的思路是这样的,我们需要自定义属性,自定义view:重写构造、onDraw方法,这些都是必须的。我们可以在onSizeChanged方法中进行计算和确认各组成内容的位置与大小。在计算好大小和位置后,在onDraw中进行绘制。
关键的是:我们的目标是将各数据绘制在各半径上,最后链接起来构成完整区域,那就需要将各半径上所有点计算出,找到对应数据对应的点的坐标,然后绘制。
1、自定义View
A、定义属性:
<!--雷达表--> <declare-styleable name="RadarChart"> <!-- 蜘蛛网线条宽度 --> <attr name="radarLineWidth" format="dimension"/> <!-- 蜘蛛网颜色 --> <attr name="radarLineColor" format="color"/> <!-- 半径分成N段--> <attr name="radarLineSegments" format="integer"/> <!-- 文字颜色 --> <attr name="radarTextColor" format="color"/> <!-- 文字字体大小--> <attr name="radarTextSize" format="integer"/> <!-- 数据展示覆盖区域颜色 --> <attr name="radarCoverColor" format="color"/> </declare-styleable>
主要就是一些线条颜色、字体颜色、大小等属性。
B、继承View,声明各属性
/** * Description: 雷达表 * Created by jia on 2017/10/18. * 人之所以能,是相信能 */public class JsRadarChart extends View { private static final String TAG = "JsRadarChart"; /** * 雷达边数 默认6 */ private int mPieceNumber = 6; /** * 外接圆半径 */ private int mRadius = 50; /** * 线条宽度 默认10 */ private int mLineWidth = 8; /** * 线条颜色 默认灰色 */ private int mLineColor = 0xffd0d6dc; /** * 半径分为4段 */ private int mLineSegments = 4; /** * 字体颜色和字体大小 */ private int mTextColor = 0xff647d91; private int mTextSize = 10; /** * 覆盖物颜色 */ private int mCoverColor = 0x55ced657; /** * 中心位置 */ private int mPositionX = 0; private int mPositionY = 0; /** * 多边形 边的角度 */ private double mAverageAngle = 0;
C、定义画笔和数据集合
注意:覆盖物区域我们使用Path实现。
/** * 三只画笔 */ private Paint mRadarPaint; private TextPaint mTextPaint; private Paint mCoverPaint; private Path mCoverPath; /** * 所有边对应的点的集合,每一个item是每条边上所有的结点的集合 */ List<RadarPoints> mRadarPointses = new ArrayList<>(); /** * 实体类集合 */ List<RadarEntry> mRadarEntries = new ArrayList<>(); /** * 覆盖物的顶点的集合 */ List<PointF> mCoverPoints = new ArrayList<>(); /** * 文字对应点的集合 */ List<PointF> mTextPoints = new ArrayList<>();
其中,创建了两个类:RadarEntry是数据实体类,RadarPoints 是每条半径上的所有点,其中用List存放各店,index表识哪条半径。
/** * 雷达图数据载体 */ public static class RadarEntry { private String title; private double level; public RadarEntry(String title, double level) { this.title = title; this.level = level; } } /** * 每一条线上的所有点集合 */ class RadarPoints { int lineIndex; List<PointF> mPointFs; public RadarPoints(int lineIndex, List<PointF> pointFs) { this.lineIndex = lineIndex; mPointFs = pointFs; } public int getLineIndex() { return lineIndex; } public void setLineIndex(int lineIndex) { this.lineIndex = lineIndex; } public List<PointF> getPointFs() { return mPointFs; } public void setPointFs(List<PointF> pointFs) { mPointFs = pointFs; } }
D、重写构造方法
public JsRadarChart(Context context) { this(context, null); } public JsRadarChart(Context context, AttributeSet attrs) { this(context, attrs, 0); } public JsRadarChart(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 获取属性类型集 TypedArray attributes = getContext().obtainStyledAttributes(attrs, R.styleable.RadarChart); mLineWidth = (int) attributes.getDimension(R.styleable.RadarChart_radarLineWidth, 5); mLineColor = attributes.getColor(R.styleable.RadarChart_radarLineColor, 0xffd0d6dc); mLineSegments = attributes.getInteger(R.styleable.RadarChart_radarLineSegments, 4); mTextColor = attributes.getColor(R.styleable.RadarChart_radarTextColor, 0xff647d91); mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, attributes.getInteger(R.styleable.RadarChart_radarTextSize, 10), getResources().getDisplayMetrics()); mCoverColor = attributes.getColor(R.styleable.RadarChart_radarCoverColor, 0x55ced6dc); init(); }
显而易见得:在构造方法中获取来自xml文件中的各属性值。
E、初始化
将各画笔根据设置的属性进行初始化。
private void init() { // 蜘蛛网 画笔初始化 mRadarPaint = new Paint(); mRadarPaint.setColor(mLineColor); mRadarPaint.setStrokeWidth(mLineWidth); mRadarPaint.setAntiAlias(true); mRadarPaint.setStyle(Paint.Style.STROKE); // 文字绘制 画笔初始化 mTextPaint = new TextPaint(); mTextPaint.setColor(mTextColor); mTextPaint.setTextSize(mTextSize); mTextPaint.setAntiAlias(true); mTextPaint.setStyle(Paint.Style.STROKE); // 覆盖物 画笔初始化 mCoverPaint = new Paint(); mCoverPaint.setColor(mCoverColor); mCoverPaint.setAntiAlias(true); mCoverPaint.setStyle(Paint.Style.FILL); mCoverPath = new Path(); }
2、在onSizeChanged中计算各组成部分位置和大小
A、中心点位置、每部分对应圆形角
// 计算中心点位置mPositionX = w / 2;mPositionY = h / 2;// 计算角度mAverageAngle = 360.0 / mPieceNumber;
B、计算文字所占大小,进而得出半径大小
// 计算出文字所占大小int textSizeMax = 0;for (RadarEntry entry : mRadarEntries) { Rect textRect = new Rect(); mTextPaint.getTextBounds(entry.title, 0, entry.title.length(), textRect); textSizeMax = Math.max(textRect.width(), textSizeMax);}// 出去文字区域,算出最合适半径mRadius = Math.min(mPositionX - textSizeMax, mPositionY);if (mRadarEntries == null || mRadarEntries.size() == 0) { throw new NullPointerException("请先设置数据集");}
因为文字和各半径处在同一条线上,而view创建后,每条线的长度就已经确定,那我们就需要将文字计算出大小,去除就是雷达半径的最佳长度。
在计算文字大小时,应该使用Rect和Paint结合计算得出。
循环各文字大小,找到最大的值,用图形半径减去最大值,就是雷达半径的最佳长度。
C、创建根据百分比计算位置的工具方法
public float getPloygonX(double angle, double percent) { return Float.parseFloat( String.valueOf( mPositionX + Math.cos(angle / 360.0 * 2 * Math.PI) * mRadius * percent)); } public float getPloygonY(double angle, double percent) { return Float.parseFloat(String.valueOf( mPositionY + Math.sin(angle / 360.0 * 2 * Math.PI) * mRadius * percent)); }
因为直角三角形一个角的邻边,等于直角边*该角的余弦值。
所以,横坐标x的值,应该是对应半径*角度的余弦值乘以百分比,当然,此图中的原点实际为外接圆的圆心,并不在屏幕的原点上,所以需要在+圆心的横坐标。
计算y值也是如此。
D、计算每条边上的结点位置,计算各数据对应点
/** * 计算每一条轴线上的所有结点 * x轴正方向为第一条轴线,顺时针旋转 */ for (int i = 0; i < mPieceNumber; i++) { List<PointF> pointFs = new ArrayList<>(); for (int j = 0; j < mLineSegments; j++) { PointF point = new PointF(); double percent = j * 1.0 / (mLineSegments - 1); point.set(getPloygonX(mAverageAngle * i, percent), getPloygonY(mAverageAngle * i, percent)); pointFs.add(point); } RadarPoints radarPoints = new RadarPoints(i, pointFs); mRadarPointses.add(radarPoints); } /** * 根据数据集计算覆盖多变形的点 */ for (int m = 0; m < mPieceNumber; m++) { PointF pointF = new PointF(); double percent = mRadarEntries.get(m).level / 100.0; pointF.set(getPloygonX(mAverageAngle * m, percent), getPloygonY(mAverageAngle * m, percent)); mCoverPoints.add(pointF); }
以上就用到了刚刚创建的根据百分比获取坐标的方法。
注意:x轴正方向为第一条轴线,顺时针旋转。
其实我们只是将每条半径上的所有节点的坐标进行了计算。
E、计算文字的位置
/** * 设置文字显示位置 */ for (int m = 0; m < mPieceNumber; m++) { PointF pointF = new PointF(); String title = mRadarEntries.get(m).title; Rect textBound = new Rect(); mTextPaint.getTextBounds(title, 0, title.length(), textBound); // 每条边最后一个点的位置,设置文字 float boundx = mRadarPointses.get(m).getPointFs().get(mLineSegments - 1).x; float boundy = mRadarPointses.get(m).getPointFs().get(mLineSegments - 1).y; if (boundx > mRadius && boundy <= mRadius) { pointF.set(getPloygonX(mAverageAngle * m, 1), getPloygonY(mAverageAngle * m, 1) - textBound.height() * 2); } else if (boundx <= mRadius && boundy <= mRadius) { pointF.set(getPloygonX(mAverageAngle * m, 1) - textBound.width(), getPloygonY(mAverageAngle * m, 1) - textBound.height() * 2); } else if (boundx <= mRadius && boundy > mRadius) { pointF.set(getPloygonX(mAverageAngle * m, 1) - textBound.width(), getPloygonY(mAverageAngle * m, 1)); } else { pointF.set(getPloygonX(mAverageAngle * m, 1), getPloygonY(mAverageAngle * m, 1)); } mTextPoints.add(pointF); }
此步骤是将文字大小计算并设置到对应List中。
我们将文字位置分了四类:类似于数学中的四各象限,不同象限的位置应该分别处理。
到这里,我们的所有计算相关的操作就做完了,接下来开始绘制。
3、将各半径、边框、文字、覆盖物进行绘制
一下步骤都在onDraw方法中执行。
A、绘制中心点、根据各节点绘制环形网
/** * 绘制中心点 */ canvas.drawPoint(mPositionX, mPositionY, mRadarPaint); /** * 绘制蜘蛛网 */ for (int i = 0; i < mLineSegments; i++) { for (int j = 0; j < mPieceNumber - 1; j++) { canvas.drawLine(mRadarPointses.get(j).getPointFs().get(i).x, mRadarPointses.get( j).getPointFs().get(i).y, mRadarPointses.get(j + 1).getPointFs().get(i).x, mRadarPointses.get( j + 1).getPointFs().get(i).y, mRadarPaint); } canvas.drawLine(mRadarPointses.get(mPieceNumber - 1).getPointFs().get(i).x, mRadarPointses.get(mPieceNumber - 1).getPointFs().get(i).y, mRadarPointses.get(0).getPointFs().get(i).x, mRadarPointses.get( 0).getPointFs().get(i).y, mRadarPaint); }
因为各节点我们已经计算得出,并且全部设置在对应List中,所以直接循环进行绘制各点,并将各点进行连接,就可以得到环形网。
B、当然还有半径
/** * 绘制轴线 */ for (int k = 0; k < mPieceNumber; k++) { canvas.drawLine(mRadarPointses.get(k).getPointFs().get(0).x, mRadarPointses.get(k).getPointFs().get(0).y, mRadarPointses.get(k).getPointFs().get(mLineSegments - 1).x, mRadarPointses.get( k).getPointFs().get(mLineSegments - 1).y, mRadarPaint); }
也是将各点连接起来就可以。
C、将覆盖物绘出
/** * 绘制数据 */ if (mCoverPoints != null && mCoverPoints.size() == mPieceNumber) { mCoverPath.reset(); mCoverPath.moveTo(mCoverPoints.get(0).x, mCoverPoints.get(0).y); for (int i = 1; i < mPieceNumber; i++) { mCoverPath.lineTo(mCoverPoints.get(i).x, mCoverPoints.get(i).y); } mCoverPath.close(); canvas.drawPath(mCoverPath, mCoverPaint); } else { throw new NullPointerException("请先设置数据集"); }
将各半径上的数据对应的点全部循环连接,但要注意,循环结束后,并没有将最后一个点和第一个点连接起来,所以需要在特意将它俩连接。完成后填充闭合图形。
到此覆盖物便画好了。
D、绘制文字
/** * 绘制文字,使用StaticLayout进行换行文字的绘制 */ for (int i = 0; i < mPieceNumber; i++) { canvas.save(); String str = mRadarEntries.get(i).title + "\n" + Math.floor(mRadarEntries.get(i).level * 10) / 10; StaticLayout layout = new StaticLayout(str, mTextPaint, 300, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true); canvas.translate(mTextPoints.get(i).x, mTextPoints.get(i).y); layout.draw(canvas); canvas.restore(); }
根据计算得出的文字位置进行绘制,不再累赘。
到此,所以的绘制全部结束!
4、暴露设置数据的方法
/** * 设置数据集,数据集的index决定位置,顺时针方向,起始角度为0度 */ public void setRadatEntries(List<RadarEntry> entries) { this.mRadarEntries = entries; mPieceNumber = entries.size(); postInvalidate(); }
注意要调用postInvalidate方法进行刷新。
5、总结
怎么样,有木有很简单,有木有感觉把中学的数学再一次拾起。哈哈。
大家的支持就是我前进的动力!
大家还可以关注我的微信公众号获取更多精彩内容。
- 用数学思维实现雷达分析图
- Qt 实现雷达图
- Andriod雷达图简单实现
- Hightchart 实现 polar 雷达图
- 数学思维导图学习方法
- 用Silverlight做雷达图
- cocos2dx实现游戏属性雷达图
- Android雷达图变种的一个实现
- [MSCHART]如何使用MSCHART实现雷达图
- iOS雷达图 iOS RadarChart实现
- iOS雷达图 iOS RadarChart实现
- echarts图表库 实现简单 雷达图
- ArcGIS API + Echarts 实现动态雷达图
- 用 VML 实现仿雷达扫描效果
- 第二人生的源码分析(九十九)雷达地图的实现
- 雷达图
- 用思维导图深入分析问题
- cocos2dx 雷达的实现
- iOS修改textField的placeholder的字体颜色
- QT整理之HelloWorld测试例子
- 多表查询
- Machine Learning part2---pandas操作
- 学习与oi之间需要一个平衡
- 用数学思维实现雷达分析图
- var_dump 加强打印数量
- Android内存分配/回收的一个问题-为什么内存使用很少的时候也GC
- 非泛型集合,泛型集合,栈,队列
- java web项目:adminsystem(1)
- shiro源码分析篇2:请求过滤,登录判断
- 洛谷10月月赛R2-T1-浮游大陆的68号岛
- 高阶函数(2)
- VMware虚拟机安装Linux操作系统