最近在做这样一个桌面应用程序,从公司后台服务器查询后,获取到设备上报的数据(每条都是字符串+JSON的形式),其中字符串里包含了设备名称、上报时间这两条信息,JSON中则包含了数据上报类型、重点关注的设备CPU温度及其他次要信息。需求呢,则是需要将这些数据中的时间、温度筛选出来制成折线图,并达到最终的一个显示效果(算是简单的数据可视化吧)。
其中参考几篇帮助较大的博文:
1.C# Chart控件,chart、Series、ChartArea曲线图绘制的重要属性
http://blog.sina.com.cn/s/blog_621e24e20101cp64.html
2.C# Chart各个属性详细解析、应用
https://blog.csdn.net/Andrewniu/article/details/78770227
3.c#改变坐标轴的标注
https://blog.csdn.net/u014683488/article/details/52106424
4.当然还有微软的官方文档
https://docs.microsoft.com/zh-cn/dotnet/api/system.web.ui.datavisualization.charting.customlabel?view=netframework-4.7.2
实际上工具使用者的需求是这样的:
①如图中所示,x轴能准确看出时间,且只显示整点的网格线,y轴则显示某个区间内的CPU温度,并且在重点关注范围能够明显区分;
②曲线上前后两点之间,若上报时间大于30分钟,则曲线不连续;
③显示温度最大值的时间及数值,并有y轴网格线穿过该点(未实现)
下面进入正文,在满足各个需求在实现过程中遇到的问题。
在此之前,需要大概了解的chart属性有ChartAreas、Series,想要画出目标图形,需要对两者的众多属性进行设置,上面参考博文中有详细解释,在我的理解中,按照结构来讲大概是如下图这样,Chart控件内可以添加多个ChartAreas,可以理解为作图区域,默认有一个为ChartAreas[0];而在各个ChartAreas中都可以有多个Series,Series是一些列点(Points)的集合,而这些点组成的集合与X轴(X axis)以及Y轴(Y axis)则构成了我们眼中的图表,即存在这样的结构:
----Chart ......一级
--------ChartAreas ......二级
------------Series ......三级
----------------Points ......四级
------------X axis ......三级
------------Y axis ......三级
在心中有个大致的认识后,还是需要动手才能创造出目标物,毕竟写代码这种事情读万卷书不如行万里路。那么就根据各个需求各个击破。
一、需求一
该需求中设计操作有,①改变x轴标签(label)②设置x轴y轴最大最小值及间隔③区分不同区间的y值。
此处主要是对ChartAreas中的Axes(轴)进行操作,先看一下Axes集合中包含的内容,进行操作“选择chart——点击‘ChartAreas’后的‘集合’——点击‘Axes’后的‘集合’”,如图:
直接贴代码
①给x轴添加标签
CustomLabel label = new CustomLabel();
label.FromPosition = i - 0.5;
label.ToPosition = i + 0.5;
//标签序号,在同一个标签中
label.RowIndex = 0;
chart1.ChartAreas[0].AxisX.CustomLabels.Add(label);
②设置x、y轴最大最小值及间隔
chart1.ChartAreas[0].AxisX.Maximum = EndHour;
chart1.ChartAreas[0].AxisX.Minimum = BeginHour;
chart1.ChartAreas[0].AxisX.Interval = 1;
chart1.ChartAreas[0].AxisY.Maximum = 120;
chart1.ChartAreas[0].AxisX.Minimum = 60;
chart1.ChartAreas[0].AxisY.Interval = 5;
③之前想的是创建两个ChartAres进行叠加,新的ares作为“背景”置于折线图后,尝试过之后发现并不容易(水平有限-.-),就退而求其次自己在excel表格中对等高的单元格填充颜色后获得所需比例的图像,按照温度的关注区间,制作了两张颜色所占高度不同比例的图,最后根据y轴数据最小值来设置不同背景;
if (Min_Y < 80)
{
chart1.ChartAreas[0].AxisY.Minimum = 60;
chart1.ChartAreas[0].BackImage = Application.StartupPath + "\\2.png";
}
else
{
chart1.ChartAreas[0].AxisY.Minimum = 80;
chart1.ChartAreas[0].BackImage = Application.StartupPath + "\\1.png";
}
至此需求一的关键实现已满足。
二、需求二
两点之间不连续的话,之前并没听说过折线图的点之间还可以不连线,则考虑到两个series之间并不存在绝对联系,则可通过添加一个新的series来实现。
if (time - lastTime > (decimal)0.5) //大于30分钟,不跟上一条点集连续,再加一条
{
Series NewSeries = new Series();
//NewSeries.ChartType = SeriesChartType.Line;
onSeries = onSeries + 1;
NewSeries.Name = "CPU温度" + onSeries;
chart1.Series.Add(NewSeries.Name);
chart1.Series[NewSeries.Name].MarkerBorderColor = Color.Honeydew;
chart1.Series[NewSeries.Name].MarkerBorderWidth = 2;
chart1.Series[NewSeries.Name].MarkerSize = 8;
chart1.Series[NewSeries.Name].MarkerColor = Color.Red;
chart1.Series[NewSeries.Name].MarkerStyle = MarkerStyle.Circle;
chart1.Series[NewSeries.Name].ChartType = SeriesChartType.Line;
}
那么需求二关键实现也满足了。
三、需求三
可能对于整个需求来说,需求三才是一个难点,之前的想法并没有把标签Label和工具提示ToolTip的作用区分开来,在尝试后认为:
①Label作为Series上点Points的一个属性,在设置后会显示在图表上表示该点的附近的一个合适位置,设置后则显示在所在ChartAres上(未找到可以隐藏该属性的方法,如Label.Visible=false这样的);
②作为ToolTip的话,设置完成后可以通过获得该点的该属性值获取设置到的文本,故在此设置此属性来显示最大值上文字。
故对每个点的ToolTip都进行设置,在轮询找到Y值最大值时,进行设置:
chart1.Series[onSeries].Points[now].ToolTip = "时间:" + X_Value[i] + "\r\n" + "值:" + Y_Value[i];
在上述代码段中,X_Value为从数据中筛选到的数据上报时间的集合,将其值设置为该点的工具提示文本,后面要做的就是只将最大值的Label设置为与ToolTip文本相关的值(要注意,ToolTip后面也是有用的,就是在鼠标移动到该点时,将ToolTip的内容显示出来),下面先设置最大值的显示:
chart1.Series[Max_Index].Points[Max_Y_Index].Label = "时间:" + chart1.Series[Max_Index].Points[Max_Y_Index].ToolTip + "\r\n" + "值:" + Max_Y;
在上述代码段中,Max_Index为最大值所在Series的序号,Max_Y_Index为最大值的点在其所在Series的序号,即最大值所在点的标签文本=最大值所在点的工具提示文本+其他描述。
接下来看似已经实现了所有需求,但在需求中漏掉了较为重要的一点,作为数据可视化来讲,怎么能只是显示一个数据呢(虽然用户需求是这样,但作为初级程序员来说,不能对自己降低要求),要做的就是上面说的,移动鼠标到一个Point,显示该点的ToolTip,直接贴代码:
private void chart1_GetToolTipText(object sender, ToolTipEventArgs e)
{
try
{
HitTestResult myTestResult = chart1.HitTest(e.X, e.Y, ChartElementType.DataPoint);//获取命中测试的结果
if (myTestResult.ChartElementType == ChartElementType.DataPoint)
{
//第几个点
int i = myTestResult.PointIndex;
DataPoint dp = myTestResult.Series.Points[i];
double XValue = chart1.Series[0].Points[i].XValue;//获取数据点的X值
//string YValue = dp.YValues[0].ToString();//获取数据点的Y值
JSONObject j;
e.Text = "序列:" + myTestResult.Series.Name + "\r\n时间:" + dp.ToolTip + "\r\n值:" + dp.YValues[0] + "\r\n";//ChartResult.Series[i].ToolTip;
}
}
catch (Exception exc)
{
}
}
显示效果的话如下:
四、总结
引用一句话来总结吧,实践是检验真理的唯一标准。古人也说,纸上谈来终觉浅,绝知此事要躬行。看的再多不如动手一试,当然也不是说在无任何基础的情况下去试,之前看到一位大牛博客中说的,当学习理论知识感到饱和或者厌倦的时候,就去动手做一些东西;当动手到毫无头绪如何进行下一步的时候,尝试去查阅理论知识来丰富自己的思维,这句话在我看来是非常赞同并在贯彻落实的。
当然,学习的过程缺少不了总结,在这个桌面应用中,主要是用于将本地磁盘中的视频与上报数据中的温度进行对比,以分析各个时间段视频的各项参数是否符合标准,其中当然不仅上述的些许操作,整个流程如下:
①操作access数据库存储配置文件
②向后台发送http请求获取上报数据
③上述的显示原始数据初步处理的结果
④获取本地视频的各项参数(帧率、大小、时长等)
⑤以时间为x轴数值,以各项参数为y轴参数,画出多条曲线加上文字分析,对比配置文件内的标准,检测在某个时间点的温度下视频的某个参数是否符合。
贴上一张最终结果的设计界面,有想法的同学私信交流。