1.前言:
一直在从事CS应用程序开发工作,随着工作需求,要对部分数据进行可视化展示,UI设计稿其中就有玫瑰图、雷达图的展示。
花了一个下午回溯原来丢掉的数学知识点。。特此将实现方法记录下。
2.效果图:
3.数据对象(RadarObj)
每个图都是由一个数据集合对象组成,从而绘制出对应的效果,对象最基本的属性要有某一维度的数值,用于在图像中展示。
public class RadarObj { public string RColor { get; set; } public string Name { get; set; } public int DataValue { get; set; } public double DataRaidus { get; set; } /// <summary> /// Series stroke /// </summary> public Brush Stroke { get { return new SolidColorBrush((Color)ColorConverter.ConvertFromString(RColor)); ; } } /// <summary> /// Series Fill /// </summary> public Brush Fill { get { return new SolidColorBrush((Color)ColorConverter.ConvertFromString(RColor)); } } }
4.绘制玫瑰图
核心逻辑在于,将玫瑰图先理解为一个饼图,然后根据数值计算出在饼图中占用的角度,以及对应的扇面半径,改动每个扇面的半径就成了玫瑰图
其中需要使用到几何的一些基本概念,工作这么多年,忘记了蛮多的,后面各种恶补。直接上代码吧。
<UserControl x:Class="Painter.NightingaleRose" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Painter" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid> <Canvas x:Name="CanvasPanel" HorizontalAlignment="Center" VerticalAlignment="Center" Background="Gray" > </Canvas> </Grid> </UserControl>
public partial class NightingaleRose : UserControl { public NightingaleRose() { InitializeComponent(); } #region Property /// <summary> /// 数据 /// </summary> public List<RadarObj> Datas { get { return (List<RadarObj>)GetValue(DatasProperty); } set { SetValue(DatasProperty, value); } } /// <summary> /// 数值的总数 /// </summary> public int Count { get { return Datas.Sum(i => i.DataValue); } } public static readonly DependencyProperty DatasProperty = DependencyProperty.Register("Datas", typeof(List<RadarObj>), typeof(NightingaleRose), new PropertyMetadata(new List<RadarObj>())); /// <summary> /// 当前绘制大区域 /// </summary> private double MaxSize { get { var par = this.Parent as FrameworkElement; return par.ActualHeight > par.ActualWidth ? par.ActualWidth : par.ActualHeight; } } /// <summary> /// 停靠间距 /// </summary> public int RoseMargin { get { return (int)GetValue(RoseMarginProperty); } set { SetValue(RoseMarginProperty, value); } } public static readonly DependencyProperty RoseMarginProperty = DependencyProperty.Register("RoseMargin", typeof(int), typeof(NightingaleRose), new PropertyMetadata(50)); /// <summary> /// 空心内环半径 /// </summary> public int RoseInsideMargin { get { return (int)GetValue(RoseInsideMarginProperty); } set { SetValue(RoseInsideMarginProperty, value); } } public static readonly DependencyProperty RoseInsideMarginProperty = DependencyProperty.Register("RoseInsideMargin", typeof(int), typeof(NightingaleRose), new PropertyMetadata(20)); /// <summary> /// 显示值标注 /// </summary> public bool ShowValuesLabel { get { return (bool)GetValue(ShowValuesLabelProperty); } set { SetValue(ShowValuesLabelProperty, value); } } public static readonly DependencyProperty ShowValuesLabelProperty = DependencyProperty.Register("ShowValuesLabel", typeof(bool), typeof(NightingaleRose), new PropertyMetadata(true)); public static readonly DependencyProperty ShowToolTipProperty = DependencyProperty.Register("ShowToolTip", typeof(bool), typeof(NightingaleRose), new PropertyMetadata(false)); /// <summary> /// 延伸线长 /// </summary> public int LabelPathLength { get { return (int)GetValue(LabelPathLengthProperty); } set { SetValue(LabelPathLengthProperty, value); } } public static readonly DependencyProperty LabelPathLengthProperty = DependencyProperty.Register("LabelPathLength", typeof(int), typeof(NightingaleRose), new PropertyMetadata(50)); #endregion Property #region Method /// <summary> /// 初始化数据 /// </summary> private void initData() { CanvasPanel.Children.Clear(); if (this.Datas != null && this.Datas.Count > 0) { this.CanvasPanel.Width = this.CanvasPanel.Height = 0; //求角度比例尺 (每个值占多大的角度 可以算到每一块图所占的角度) var angelScale = 360.00 / Datas.Sum(i => i.DataValue); //最大半径 var maxRadius = (MaxSize / 2) - RoseMargin - (ShowValuesLabel ? LabelPathLength : 0); //半径比例尺 (值和比例尺相乘等于每一块图的半径) var radiusScale = maxRadius / Datas.Max(o => o.DataValue); //计算半径宽度值 for (int i = 0; i < Datas.Count; i++) { Datas[i].DataRaidus = Datas[i].DataValue * radiusScale; } //扇形角度初始化 double angleSectorStart = 0; double angleSectorEnd = 0; //循环绘制扇形区域 int scaleTimeSpan = 0; int pathTimespan = 0; int textTimeSpan = 0; for (int index = 0; index < Datas.Count; index++) { //计算扇形角度 if (index == 0) { angleSectorStart = 0; angleSectorEnd = Datas[index].DataValue * angelScale; } else if (index + 1 == Datas.Count) { angleSectorStart += Datas[index - 1].DataValue * angelScale; angleSectorEnd = 360; } else { angleSectorStart += Datas[index - 1].DataValue * angelScale; angleSectorEnd = angleSectorStart + Datas[index].DataValue * angelScale; } var currentRadius = RoseInsideMargin + Datas[index].DataRaidus; //计算扇形点位,用于绘制PATH Point ptOutSideStart = GetPoint(currentRadius, angleSectorStart * Math.PI / 180); Point ptOutSideEnd = GetPoint(currentRadius, angleSectorEnd * Math.PI / 180); Point ptInSideStart = GetPoint(RoseInsideMargin, angleSectorStart * Math.PI / 180); Point ptInSideEnd = GetPoint(RoseInsideMargin, angleSectorEnd * Math.PI / 180); if (string.IsNullOrEmpty(Datas[index].RColor) ) Datas[index].RColor = ChartColorPool.ColorStrings[index]; Path pthSector = new Path() { Fill = Datas[index].Fill }; //PATH数据格式 M0,100 L50,100 A50,50 0 0 1 100,50 L100,0 A100,100 0 0 0 0,100 Z StringBuilder datastrb = new StringBuilder(); #region BuilderPathData datastrb.Append("M"); datastrb.Append(ptOutSideStart.X.ToString()); datastrb.Append(","); datastrb.Append(ptOutSideStart.Y.ToString()); datastrb.Append(" L"); datastrb.Append(ptInSideStart.X.ToString()); datastrb.Append(","); datastrb.Append(ptInSideStart.Y.ToString()); datastrb.Append(" A"); datastrb.Append(RoseInsideMargin.ToString()); datastrb.Append(","); datastrb.Append(RoseInsideMargin.ToString()); datastrb.Append(" 0 0 1 "); datastrb.Append(ptInSideEnd.X.ToString()); datastrb.Append(","); datastrb.Append(ptInSideEnd.Y.ToString()); datastrb.Append(" L"); datastrb.Append(ptOutSideEnd.X.ToString()); datastrb.Append(","); datastrb.Append(ptOutSideEnd.Y.ToString()); datastrb.Append(" A"); datastrb.Append(currentRadius.ToString()); datastrb.Append(","); datastrb.Append(currentRadius.ToString()); datastrb.Append(" 0 0 0 "); datastrb.Append(ptOutSideStart.X.ToString()); datastrb.Append(","); datastrb.Append(ptOutSideStart.Y.ToString()); datastrb.Append(" Z"); #endregion BuilderPathData try { pthSector.Data = (Geometry)new GeometryConverter().ConvertFromString(datastrb.ToString()); } catch (Exception exp) { } //设置扇形显示的动画 AnimationUtils.FloatElement(pthSector,1, 200, pathTimespan += 200); AnimationUtils.ScaleRotateEasingAnimationShow(pthSector,0.1,1,1500, scaleTimeSpan += 200,null ); CanvasPanel.Children.Add(pthSector); if (ShowValuesLabel) { //计算延伸线角度 double lbPathAngle = angleSectorStart + (angleSectorEnd - angleSectorStart) / 2; //起点 Point ptLbStart = GetPoint(currentRadius, lbPathAngle * Math.PI / 180); //终点 Point ptLbEnd = GetPoint(maxRadius + LabelPathLength, lbPathAngle * Math.PI / 180); Path pthLb = new Path() { Stroke = Datas[index].Stroke, StrokeThickness = 1 }; pthLb.Data = (Geometry)new GeometryConverter().ConvertFromString(string.Format("M{0},{1} {2},{3}", ptLbStart.X.ToString(), ptLbStart.Y.ToString(), ptLbEnd.X.ToString(), ptLbEnd.Y.ToString())); double dur = (textTimeSpan += 200) + 1500; AnimationUtils.CtrlDoubleAnimation(pthLb, 1000, dur); CanvasPanel.Children.Add(pthLb); SetLabel(Datas[index], ptLbEnd, dur); } } this.SizeChanged -= RadarControl_SizeChanged; this.SizeChanged += RadarControl_SizeChanged; } } public void InitalControl() { } /// <summary> /// 初始化数据 /// </summary> /// <param name="dataobj"></param> public void SetData(object dataobj) { this.Datas = (dataobj) as List<RadarObj>; this.initData(); } private void RadarControl_SizeChanged(object sender, SizeChangedEventArgs e) { initData(); } #endregion Method #region Compare /// <summary> /// 计算点位 /// </summary> /// <param name="radius"></param> /// <param name="angel"></param> /// <returns></returns> private Point GetPoint(double radius, double angel) { return new Point(radius * Math.Cos(angel), radius * Math.Sin(angel)); } private void SetLabel(RadarObj obj, Point location,double duration) { //计算偏移量 bool x = true; bool y = true; if (location.X < 0) x = false; if (location.Y < 0) y = false; //obj.Name + " " + TextBlock txb = new TextBlock() { Text = " "+obj.Name+" "+Getbfb(Count.ToString(), obj.DataValue.ToString(), 2)+ " ", Foreground = this.Foreground, FontSize = this.FontSize }; Size s = ControlSizeUtils.GetTextAreaSize(txb.Text, this.FontSize); CanvasPanel.Children.Add(txb); AnimationUtils.CtrlDoubleAnimation(txb, 1000, duration); if (location.X > -5 && location.X < 5) Canvas.SetLeft(txb, location.X - (s.Width / 2)); else Canvas.SetLeft(txb, location.X + (x ? 0 : -(s.Width))); if (location.Y > -5 && location.Y < 5) Canvas.SetTop(txb, location.Y - (s.Height / 2)); else Canvas.SetTop(txb, location.Y + (y ? 0 : -(s.Height))); } /// <summary> /// 计算百分比 /// </summary> /// <param name="zs">总数</param> /// <param name="tj">当前项的值</param> /// <param name="num">保留的小数点几位</param> /// <returns></returns> public static string Getbfb(string zs, string tj, int num) { try { if (zs.Equals("0")) { return "0"; } double bfb = (double.Parse(tj) / double.Parse(zs)) * 100; if (bfb >= 100) { bfb = 100; } return Math.Round(bfb, num).ToString() + "%"; } catch (Exception ex) { return "0%"; } } #endregion Compare }
调用示例
在主窗体grid里面丢个按钮,按钮点击之后执行下列代码
private void RoseClick(object sender, RoutedEventArgs e) { NightingaleRose rdc = new NightingaleRose(); this.GrdMain.Children.Clear(); this.GrdMain.Children.Add(rdc); rdc.SetData(CrData()); } private List<RadarObj> CrData() { List<RadarObj> list = new List<RadarObj>(); list.Add(new RadarObj() { Name="A", DataValue= rdm.Next(20,100) }); list.Add(new RadarObj() { Name = "B", DataValue = rdm.Next(20, 100) }); list.Add(new RadarObj() { Name = "C", DataValue = rdm.Next(20, 100) }); list.Add(new RadarObj() { Name = "D", DataValue = rdm.Next(20, 100) }); list.Add(new RadarObj() { Name = "E", DataValue = rdm.Next(20, 100) }); list.Add(new RadarObj() { Name = "F", DataValue = rdm.Next(20, 100) }); list.Add(new RadarObj() { Name = "F", DataValue = rdm.Next(20, 100) }); return list; }