Sankey图可以分析能量数据流,网页形式的Sankey图库很多,而相应的WPF库却很少,这里推荐一个开源的WPF Sankey图:
GitHub - iou90/SankeyDiagram: A powerful and easy to use WPF library for drawing Sankey diagram
这个项目已经有好些年了,经过测试,功能还是比较完善的,下面对其部分功能进行修改(含一个Bug的屏蔽)。
加重节点颜色的显示
原始的显示如下:
新的显示效果如下:
没错,就是添加了加重颜色的矩形显示,这里主要在SankeyStyleManager.cs中修改:
public void SetNodeBrush(SankeyNode node)
{
var brushCheck = diagram.NodeBrushes != null && diagram.NodeBrushes.Keys.Contains(node.Name);
if (brushCheck)
{
node.Shape.Fill = diagram.NodeBrushes[node.Name].CloneCurrentValue();
}
else
{
if (diagram.UsePallette != SankeyPalette.None)
{
var solidBrush = defaultNodeLinksPalette[DefaultNodeLinksPaletteIndex].CloneCurrentValue() as SolidColorBrush;
//设置边框颜色
var fillColor = solidBrush.Color;
fillColor.A = (byte)255;
node.Shape.StrokeThickness = diagram.NodeThickness / 2;
node.Shape.Stroke =new SolidColorBrush(fillColor);
node.Shape.Fill = solidBrush;
DefaultNodeLinksPaletteIndex++;
if (DefaultNodeLinksPaletteIndex >= defaultNodeLinksPalette.Count)
{
DefaultNodeLinksPaletteIndex = 0;
}
}
}
node.OriginalBrush = node.Shape.Fill.CloneCurrentValue();
}
同时为了保证标签文字正确的偏移,在SankeyDiagramAssist.cs中修改CreateDiagram()
在if (diagram.SankeyFlowDirection == FlowDirection.TopToBottom){...}
Else{}中
将Canvas.SetRight(node.Label, node.Shape.Width);修改为
Canvas.SetRight(node.Label, diagram.NodeThickness);
将Canvas.SetLeft(node.Label, node.X + node.Shape.Width);修改为
Canvas.SetLeft(node.Label, node.X + diagram.NodeThickness);
调整颜色盘的颜色
Sankey图默认从颜色盘中获取颜色,并依次进行分配,这需要确保相应的属性设置:
UsePallette="NodesLinks"
在SankeyStyleManager.cs中的GetNodeLinksPalette(double opacity)可以预设各种颜色,例如:
return new List<Brush>()
{
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#49B4FD")) { Opacity = opacity },//0095fb 蓝色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF7D7D")) { Opacity = opacity },//ff0000 红色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#33C57C")) { Opacity = opacity },//60e8a4 绿色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#EBB13C")) { Opacity = opacity },//ffa200 橙色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#12E0BC")) { Opacity = opacity },//00f2c8 浅绿色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7373ff")) { Opacity = opacity }, //紫色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#91bc61")) { Opacity = opacity }, //青色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#dc89d9")) { Opacity = opacity }, //粉色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F2E510")) { Opacity = opacity },//fff100 黄色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#44c5f1")) { Opacity = opacity }, //浅蓝色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#19D904")) { Opacity = opacity },//85e91f 浅绿色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#00b192")) { Opacity = opacity }, //蓝绿色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1cbe65")) { Opacity = opacity }, //1cbe65绿色
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#278bcc")) { Opacity = opacity },
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#954ab3")) { Opacity = opacity },
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#f3bc00")) { Opacity = opacity },
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#e47403")) { Opacity = opacity },//e47403
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#EF5936")) { Opacity = opacity },//ce3e29
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#A1C5D3")) { Opacity = opacity },//d8dddf
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#ffb5ff")) { Opacity = opacity }
};
设置连接线为渐变色
原始图如下:
修改后如下:
在SankeyDiagramAssist.cs中的CreateNodesAndLinks(IEnumerable<SankeyData> datas)中修改
在shape.Tag = new SankeyLinkFinder(data.From, data.To);下面添加代码
//设置渐变色
var fromColor = (fromNode.Shape.Fill as SolidColorBrush).Color;
var toColor = (toNode.Shape.Fill as SolidColorBrush).Color;
fromColor.A = (int)(255 * 0.5); //这里的0.5为正常显示时的透明度,与颜色盘中的透明度一致
toColor.A = (int)(255 * 0.5); //这里的0.5为正常显示时的透明度,与颜色盘中的透明度一致
var grdBrush = new LinearGradientBrush(fromColor,toColor,0);
然后将shape.Fill = diagram.UsePallette != SankeyPalette.None ? fromNode.Shape.Fill.CloneCurrentValue()中的fromNode.Shape.Fill.CloneCurrentValue()修改为grdBrush即可。
调整连接线在弯曲位置变窄的效果
通过设置属性LinkCurvature即可,值在0-1之间,越大效果越明显,建议值为0.8
去掉对第三方库的引用
源库引用了第三方库的MeasureHepler.cs类,这里直接粘贴上源码
public class TextInfo
{
public string Text { get; set; }
public System.Windows.FlowDirection FlowDirection { get; set; }
public Typeface Typeface { get; set; }
public double FontSize { get; set; }
public Brush Foreground { get; set; }
public Thickness Margin { get; set; }
}
public static class MeasureHepler
{
public static Size MeasureString(TextInfo info, CultureInfo culture)
{
if (info == null)
{
throw new ArgumentNullException("information");
}
var formattedText = new FormattedText(info.Text, culture, info.FlowDirection, info.Typeface, info.FontSize, info.Foreground, (new DpiScale(1, 1)).PixelsPerDip);
return new Size(formattedText.Width + info.Margin.Left + info.Margin.Right, formattedText.Height + info.Margin.Top + info.Margin.Bottom);
}
public static Size MeasureString(string text, Style style, CultureInfo culture)
{
if (style == null || style.Setters == null)
{
throw new ArgumentNullException("style or style.Setters is null");
}
var flowDirection = System.Windows.FlowDirection.LeftToRight;
FontFamily fontFamily = new FontFamily("Segoe UI");
double emSize = 12.0;
FontStyle style2 = FontStyles.Normal;
FontWeight weight = FontWeights.Regular;
FontStretch stretch = FontStretches.Normal;
Thickness thickness = new Thickness(0.0);
Brush foreground = new SolidColorBrush(Colors.Black);
foreach (SetterBase setter2 in style.Setters)
{
Setter setter = (Setter)setter2;
switch (setter.Property.Name)
{
case "FlowDirectoin":
flowDirection = (System.Windows.FlowDirection)setter.Value;
break;
case "FontFamily":
fontFamily = (FontFamily)setter.Value;
break;
case "FontSize":
emSize = (double)setter.Value;
break;
case "FontStyle":
style2 = (FontStyle)setter.Value;
break;
case "FontWeight":
weight = (FontWeight)setter.Value;
break;
case "FontStretch":
stretch = (FontStretch)setter.Value;
break;
case "Foreground":
foreground = (Brush)setter.Value;
break;
case "Margin":
thickness = (Thickness)setter.Value;
break;
}
}
var formattedText = new FormattedText(text, culture, flowDirection, new Typeface(fontFamily, style2, weight, stretch), emSize, foreground, (new DpiScale(1, 1)).PixelsPerDip);
return new Size(formattedText.Width + thickness.Left + thickness.Right, formattedText.Height + thickness.Top + thickness.Bottom);
}
}
当添加互为From/To的数据时,程序会进入死循环
在SankeyDiagramAssist.cs中的CreateNodesAndLinks(IEnumerable<SankeyData> datas)修改如下
var addedData=new List<SankeyData>();
foreach (var data in datas)
{
if (addedData.Exists(n=>n.From==data.To && n.To==data.From))
continue;
if (!currentSliceNodes.Exists(n => n.Name == data.From))
{
currentSliceNodes.Add(CreateNode(data, data.From,true));
addedData.Add(data);
}
if (!currentSliceNodes.Exists(n => n.Name == data.To))
{
currentSliceNodes.Add(CreateNode(data, data.To, false));
addedData.Add(data);
}
上面的代码通过添加addedData集合,检测如果存在互为From/To的数据时跳过
修改后最终的显示效果如下
选中节点时高亮显示所有相关的连接和加大文本字体
也可以只高亮显示单个连接和对应的文本字体加大
目前做了一些数据测试后发现,对于A->B->C->D...这种一层一层数据传递的,上述的库表现的很完美,但如果出现那种A->B,又出现A->C这种跨层次的数据传递,图形的避让和防重叠设计则表现得不怎样了,可能需要进一步优化。