自开发数据可视化平台,目前只是一个简单的Demo版。
可以动态的设置数据源,动态绑定字段,
目前自适应布局逻辑还没想出来,是通过代码写死
在实现过程中遇到的难点和亮点工作
1.Combobox自定义搜索逻辑
2.Combobox绑定数据源过大是出现卡顿解决方法
3.气泡图绘制算法,如何保证整体居中
4.使用SVG Geometry绘制几何形状
5,如何绘制标签文本及定位
6.度量的刻度最小刻度确定,保证刻度线介于5~10个之间
7,确定起始刻度,支持刻度从非0位置开始,保证所有刻度值是最小刻度的整数倍
8.利用枚举值动态绑定按钮,动态绑定路由事件
9,支持配色方案,借鉴了Tableau Deafult配色
以后根据需要及自由时间安排不断补充细节和扩展创新功能
做再多的图其实没有什么本质区别,无非是图形之间内在结构和逻辑复杂一点,真正需要创新的点和努力的方向在于如如何更方便地使用数据提高工作效率,提高软件探索性分析,解决实际问题的能力。
可以看一下可视化效果
增加基金图时间筛选按钮,默认本年
近一年
近3年
源代码路径
InfoVhttps://github.com/PengChen01/InfoV调用代码
View.Create.DrawPie(this.windowCanvas,new Point(10, 50), new Size(300, 250), new View.BindingInfo() { Dimension = new List<string> { "Type" }, Measure = new List<string> { "Count" } }, tab,"基金类型占比情况饼图");
饼图实现代码
/// <summary>
/// 制作饼图
/// </summary>
/// <param name="start">左上角位置</param>
/// <param name="size">环图容器大小</param>
/// <param name="tab">绑定的数据</param>
/// <returns></returns>
public static void DrawPie(Canvas windowcanvas, Point start, Size size, BindingInfo bindingInfo, IEnumerable<object> datasource, string title = null)
{
//显示标题
int titleHeight = 30;
int HorizatalLableWidth = 20;
int VeritalLableWidth = 20;
Point center = new Point(size.Width / 2, (size.Height + titleHeight- HorizatalLableWidth) / 2);
int radius = (int)(Math.Min(size.Width- VeritalLableWidth*2, (size.Height - titleHeight - HorizatalLableWidth)) / 2) - 5;
PathGeometry pathGeometry = new PathGeometry();
var dataSource = datasource.Select(item => new { Tag = (string)item.GetType().GetProperty(bindingInfo.Dimension[0]).GetValue(item, null), Value = Convert.ToDouble(item.GetType().GetProperty(bindingInfo.Measure[0]).GetValue(item, null)) });
double sum = dataSource.Sum(key => key.Value);
Border border = new Border()
{
Width = size.Width,
Height = size.Height,
Background = new SolidColorBrush(Color.FromRgb(245, 245, 245)),
CornerRadius = new CornerRadius(5),
Effect = new DropShadowEffect()
{
Color = Colors.LightGray,
BlurRadius = 15
}
};
Canvas canvas = new Canvas();
border.Child = canvas;
if (!string.IsNullOrWhiteSpace(title))
{
TextBlock t = new TextBlock();
t.Text = title;
t.FontWeight = FontWeights.Bold;
t.FontSize = 15;
t.TextWrapping = TextWrapping.Wrap;
//t.RenderTransformOrigin = new Point(0.0, 1.0);
t.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
t.RenderTransform = new TranslateTransform((size.Width - t.DesiredSize.Width) / 2, (30 - t.DesiredSize.Height) / 2);
canvas.Children.Add(t);
}
double dsum = 0;
foreach (var item in dataSource)
{
double startAngle = (dsum / sum * 2 - 0.5) * Math.PI;
dsum += item.Value;
double endAngle = (dsum / sum * 2 - 0.5) * Math.PI;
Point startPointFar = new Point(center.X + radius * Math.Cos(startAngle), center.Y + radius * Math.Sin(startAngle));
Point endPointFar = new Point(center.X + radius * Math.Cos(endAngle), center.Y + radius * Math.Sin(endAngle));
List<PathSegment> segments = new List<PathSegment>();
segments.Add(new ArcSegment(endPointFar, new Size(radius, radius), 0, (endAngle - startAngle) > Math.PI, SweepDirection.Clockwise, false));
segments.Add(new LineSegment(center, false));
Path path = new Path
{
//Stroke = Brushes.Transparent,
//StrokeThickness = 2,
Data = new PathGeometry(new List<PathFigure> { new PathFigure(startPointFar, segments, true) }),
Fill = new SolidColorBrush(ColorCommon.GetNextColor()),
ToolTip = new ToolTip()
{
Content = new StackPanel()
{
Children =
{
new TextBlock() { Text = $"{item.Tag}" , FontWeight = FontWeights.Bold},
new TextBlock() { Text = $"总量: {item.Value}"},
new TextBlock() { Text = $"占比: {(item.Value/sum).ToString("P2")}"},
}
},
Placement = PlacementMode.MousePoint,
StaysOpen = true
}
};
//path.Triggers.Add(new DataTrigger() { Binding = new Binding() { Source = path, } })
canvas.Children.Add(path);
}
windowcanvas.Children.Add(border);
Canvas.SetLeft(border, start.X);
Canvas.SetTop(border, start.Y);
}
效果图
气泡图效果
气泡图实现代码
public static void DrawBubble(Canvas windowcanvas, Point start, Size size, BindingInfo bindingInfo, IEnumerable<object> datasource, string title = null)
{
//显示标题
int titleHeight = 30;
int HorizatalLableWidth = 20;
int VeritalLableWidth = 20;
Border border = new Border()
{
Width = size.Width,
Height = size.Height,
Background = new SolidColorBrush(Color.FromRgb(245, 245, 245)),
CornerRadius = new CornerRadius(5),
Effect = new DropShadowEffect()
{
Color = Colors.LightGray,
BlurRadius = 15
}
};
Canvas canvas = new Canvas();
border.Child = canvas;
if (!string.IsNullOrWhiteSpace(title))
{
TextBlock t = new TextBlock();
t.Text = title;
t.FontWeight = FontWeights.Bold;
t.FontSize = 15;
t.TextWrapping = TextWrapping.Wrap;
//t.RenderTransformOrigin = new Point(0.0, 1.0);
t.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
t.RenderTransform = new TranslateTransform((size.Width - t.DesiredSize.Width) / 2, (30 - t.DesiredSize.Height) / 2);
canvas.Children.Add(t);
}
List<BubbleStructrue> dataSource = datasource.Select(item => new BubbleStructrue { Label = (string)item.GetType().GetProperty(bindingInfo.Dimension[0]).GetValue(item, null), Value = Convert.ToDouble(item.GetType().GetProperty(bindingInfo.Measure[0]).GetValue(item, null)) }).ToList();
double sum = dataSource.Max(key => key.Value);
if (dataSource.Count > 1000)
{
throw new Exception("数量限制,无法展示");
}
double lastChildcosAngle = 1;
double lastChildsinAngle = 0;
double lastChildRadius = 0;
Point OrigPoint = new Point(0, 0);
double OrigRadius = 0;
double cosAngle = 0;
double sinAngle = 0;
for (int i = 0; i < dataSource.Count; i++)
{
var item = dataSource[i];
item.Radius = item.Value / sum *60;
if (i == 0)
{
OrigRadius = item.Radius;
item.X = 0;
item.Y = 0;
}
else if (i==1)
{
cosAngle = lastChildcosAngle;
sinAngle = lastChildsinAngle;
item.X = OrigPoint.X + (OrigRadius + item.Radius) * cosAngle;
item.Y = OrigPoint.Y + (OrigRadius + item.Radius) * sinAngle;
lastChildRadius = item.Radius;
}
else
{
double d1 = OrigRadius * (OrigRadius + item.Radius + lastChildRadius);
double d2 = item.Radius * lastChildRadius;
double dcosAngle = (d1 - d2) / (d1 + d2);
double dsinAngle = Math.Sqrt(1 - dcosAngle * dcosAngle);
cosAngle = (dcosAngle * lastChildcosAngle - dsinAngle * lastChildsinAngle);
sinAngle = (lastChildsinAngle * dcosAngle + dsinAngle * lastChildcosAngle);
item.X = OrigPoint.X + (OrigRadius + item.Radius) * cosAngle;
item.Y = OrigPoint.Y + (OrigRadius + item.Radius) * sinAngle;
lastChildcosAngle = cosAngle;
lastChildsinAngle = sinAngle;
lastChildRadius = item.Radius;
}
}
double boxXMin = dataSource.Min(s => s.X - s.Radius);
double boxWidth = dataSource.Max(s => s.X + s.Radius) - boxXMin;
double boxYMin = dataSource.Min(s => s.Y - s.Radius);
double boxHeight = dataSource.Max(s => s.Y + s.Radius) - boxYMin;
foreach (var item in dataSource)
{
Ellipse ellipse = new Ellipse()
{
Width = item.Radius * 2* (size.Width - 2 * VeritalLableWidth) / boxWidth,
Height = item.Radius * 2* (size.Height - titleHeight - HorizatalLableWidth)/ boxHeight
};
ellipse.RenderTransform = new TranslateTransform(VeritalLableWidth + (size.Width-2*VeritalLableWidth) *(item.X - item.Radius- boxXMin) / boxWidth, titleHeight+ (size.Height - titleHeight-HorizatalLableWidth) * (item.Y - item.Radius - boxYMin) / boxHeight);
ellipse.ToolTip = new ToolTip()
{
Content = new StackPanel()
{
Children =
{
new TextBlock() { Text = $"{item.Label}" , FontWeight = FontWeights.Bold},
new TextBlock() { Text = $"总量: {item.Value}"}
}
},
Placement = PlacementMode.MousePoint,
StaysOpen = true
};
ellipse.Fill = new SolidColorBrush(ColorCommon.GetNextColor());
canvas.Children.Add(ellipse);
}
windowcanvas.Children.Add(border);
Canvas.SetLeft(border, start.X);
Canvas.SetTop(border, start.Y);
}
class BubbleStructrue
{
#region 属性
/// <summary>
/// 气泡坐标
/// </summary>
public double X { get; set; }
public double Y { get; set; }
/// <summary>
/// 气泡标签
/// </summary>
public string Label { get; set; }
/// <summary>
/// 指标值
/// </summary>
public double Value { get; set; }
/// <summary>
/// 气泡绑定
/// </summary>
public double Radius { get; set; }
/// <summary>
/// 该点所环绕的父亲
/// </summary>
Point Father { get; set; }
/// <summary>
/// 开始环绕角度
/// </summary>
double StartAngle{ get; set; }
/// <summary>
/// 是否为顺时针环绕方向
/// </summary>
bool IsClockwise { get; set; }
/// <summary>
/// 最后一个环绕剩余空间最大半径
/// </summary>
double LastMaxFreeRadius { get; set; }
#endregion
//public BubbleStructrue(IEnumerable<object> datasource, BindingInfo bindingInfo)
//{
// var dataSource = datasource.Select(item => new { Tag = (string)item.GetType().GetProperty(bindingInfo.Dimension[0]).GetValue(item, null), Value = Convert.ToDouble(item.GetType().GetProperty(bindingInfo.Measure[0]).GetValue(item, null)) });
// double sum = dataSource.Sum(key => key.Value);
// if (dataSource.Count() > 1000)
// {
// throw new Exception("数量限制,无法展示");
// }
// //父点的索引
// //int MaxPointIndex = 0;
// //绕父点旋绕的起始角度
// this.StartAngle = 0;
// this.value = dataSource.ElementAt(0).Value;
// this.Label = dataSource.ElementAt(0).Tag;
// this.IsClockwise = true;
// this.Point = new Point(0, 0);
// this.Radius = this.value / sum;
// DoSurround(this);
// int i = 1;
// double currentangle = this.StartAngle;
// while (i< dataSource.Count())
// {
// var item = dataSource.ElementAt(i);
// double cosAngle = Math.Cos(currentangle);
// double sinAngle = Math.Sin(currentangle);
// double item.Radius = item.Value / sum;
// Point x1 = new Point(this.Point.X+(this.Radius + item.Radius) * cosAngle, this.Point.Y + (this.Radius + item.Radius) * sinAngle);
// item = dataSource.ElementAt(i+1);
// double radius2 = item.Value / sum;
// double d1 = this.Radius * (this.Radius + item.Radius + radius2);
// double d2 = item.Radius * radius2;
// double cosAngle2 = (d1 - d2)/(d1 + d2);
// double sinAngle2 = Math.Sqrt(1 - cosAngle2 * cosAngle2);
// new Point(this.Point.X + (this.Radius + radius2) * (cosAngle2*cosAngle-sinAngle2*sinAngle), this.Point.Y + (this.Radius + radius2) *(sinAngle*cosAngle2+sinAngle2*cosAngle));
// }
//}
//static void DoSurround(BubbleStructrue bubbleStructrue)
//{
// //bubbleStructrue.
//}
}