{"id":2706,"date":"2021-09-09T14:42:58","date_gmt":"2021-09-09T14:42:58","guid":{"rendered":"https:\/\/mindfusion.eu\/blog\/?p=2706"},"modified":"2021-09-09T14:45:50","modified_gmt":"2021-09-09T14:45:50","slug":"sankey-diagram-in-wpf","status":"publish","type":"post","link":"https:\/\/mindfusion.dev\/blog\/sankey-diagram-in-wpf\/","title":{"rendered":"Sankey diagram in WPF"},"content":{"rendered":"\n<p>In this post we&#8217;ll show one possible way of using MindFusion WPF Diagram with custom model objects, and will create a simple Sankey diagram as an example.<\/p>\n\n\n\n<!--more-->\n\n\n\n<p>Start by creating a new WPF project and adding the MindFusion.Diagramming.Wpf package in NuGet Package Manager. After installing the package, you should be able to add a Diagram element in Xaml:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>xmlns:d=\"http:\/\/mindfusion.dev\/diagramming\/wpf\"\n...\n&lt;Grid&gt;\n\t&lt;d:Diagram x:Name=\"diagram\"&gt;&lt;\/d:Diagram&gt;\n&lt;\/Grid&gt;<\/code><\/pre>\n\n\n\n<p>Now let&#8217;s define some model classes that raise an event when the diagram view need to change:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ModelBase : INotifyPropertyChanged\n{\n\tprotected void RaisePropertyChanged(string propertyName)\n\t{\n\t\tif (PropertyChanged != null)\n\t\t\tPropertyChanged(this, new PropertyChangedEventArgs(propertyName));\n\t}\n\n\tpublic event PropertyChangedEventHandler PropertyChanged;\n}\n\nclass SankeyFlow : ModelBase\n{\n\tpublic event EventHandler LayoutChanged;\n\n\tpublic double Amount\n\t{\n\t\tget { return amount; }\n\t\tset\n\t\t{\n\t\t\tif (amount != value)\n\t\t\t{\n\t\t\t\tamount = value;\n\t\t\t\tRaisePropertyChanged(\"Amount\");\n\n\t\t\t\tif (LayoutChanged != null)\n\t\t\t\t\tLayoutChanged(this, EventArgs.Empty);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic Color Color\n\t{\n\t\tget { return color; }\n\t\tset\n\t\t{\n\t\t\tif (color != value)\n\t\t\t{\n\t\t\t\tcolor = value;\n\t\t\t\tRaisePropertyChanged(\"Color\");\n\t\t\t}\n\t\t}\n\t}\n\n\tinternal SankeyNode Source { get; set; }\n\tinternal SankeyNode Target { get; set; }\n\n\tprivate double amount;\n\tprivate Color color = Colors.Red;\n}\n\nclass SankeyNode : ModelBase\n{\n\tpublic SankeyNode()\n\t{\n\t\tInFlows = new ObservableCollection&lt;SankeyFlow&gt;();\n\t\tInFlows.CollectionChanged += OnInFlowsChanged;\n\n\t\tOutFlows = new ObservableCollection&lt;SankeyFlow&gt;();\n\t\tOutFlows.CollectionChanged += OnOutFlowsChanged;\n\t}\n\n\tvoid OnInFlowsChanged(object sender, NotifyCollectionChangedEventArgs e)\n\t{\n\t\tif (e.OldItems != null)\n\t\t{\n\t\t\tforeach (SankeyFlow oldFlow in e.OldItems)\n\t\t\t\toldFlow.LayoutChanged -= OnFlowLayoutChanged;\n\t\t}\n\n\t\tif (e.NewItems != null)\n\t\t{\n\t\t\tforeach (SankeyFlow newFlow in e.NewItems)\n\t\t\t\tnewFlow.LayoutChanged += OnFlowLayoutChanged;\n\t\t}\n\n\t\tif (LayoutChanged != null)\n\t\t\tLayoutChanged(this, EventArgs.Empty);\n\t}\n\n\tvoid OnOutFlowsChanged(object sender, NotifyCollectionChangedEventArgs e)\n\t{\n\t\tif (e.OldItems != null)\n\t\t{\n\t\t\tforeach (SankeyFlow oldFlow in e.OldItems)\n\t\t\t\toldFlow.LayoutChanged -= OnFlowLayoutChanged;\n\t\t}\n\n\t\tif (e.NewItems != null)\n\t\t{\n\t\t\tforeach (SankeyFlow newFlow in e.NewItems)\n\t\t\t\tnewFlow.LayoutChanged += OnFlowLayoutChanged;\n\t\t}\n\n\t\tif (LayoutChanged != null)\n\t\t\tLayoutChanged(this, EventArgs.Empty);\n\t}\n\n\tvoid OnFlowLayoutChanged(object sender, EventArgs e)\n\t{\n\t\tif (LayoutChanged != null)\n\t\t\tLayoutChanged(this, EventArgs.Empty);\n\t}\n\n\tinternal void CalcFlow()\n\t{\n\t\tTotalInFlow = InFlows.Aggregate(\n\t\t\t0.0, (sum, flow) =&gt; sum + flow.Amount);\n\t\tTotalOutFlow = OutFlows.Aggregate(\n\t\t\t0.0, (sum, flow) =&gt; sum + flow.Amount);\n\n\t\tTotalFlow = Math.Max(TotalInFlow, TotalOutFlow);\n\n\t\tforeach (var flow in InFlows)\n\t\t\tflow.Target = this;\n\t\tforeach (var flow in OutFlows)\n\t\t\tflow.Source = this;\n\t}\n\n\tpublic event EventHandler LayoutChanged;\n\n\tpublic ObservableCollection&lt;SankeyFlow&gt; InFlows { get; private set; }\n\tpublic ObservableCollection&lt;SankeyFlow&gt; OutFlows { get; private set; }\n\n\tinternal double TotalInFlow { get; set; }\n\tinternal double TotalOutFlow { get; set; }\n\tinternal double TotalFlow { get; set; }\n\n\tpublic Color Color\n\t{\n\t\tget { return color; }\n\t\tset\n\t\t{\n\t\t\tif (color != value)\n\t\t\t{\n\t\t\t\tcolor = value;\n\t\t\t\tRaisePropertyChanged(\"Color\");\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate Color color = Colors.DarkBlue;\n}\n\n...<\/code><\/pre>\n\n\n\n<p>Create a DiagramAdapter class that will visualize the model using MindFusion.Diagramming API. To avoid recalculating positions on each change of the model structure, the LayoutChanged handler will call Dispatcher.BeginInvoke to rearrange diagram on next iteration of the message loop. This allow us to update the diagram just once for a batch of changes in the model:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class DiagramAdapter\n{\n\tpublic DiagramAdapter(SankeyModel model, Diagram diagram)\n\t{\n\t\tthis.model = model;\n\t\tthis.diagram = diagram;\n\n\t\tmodel.LayoutChanged += OnModelLayoutChanged;\n\t}\n\n\tvoid OnModelLayoutChanged(object sender, EventArgs e)\n\t{\n\t\tif (arrangePending)\n\t\t\treturn;\n\n\t\tarrangePending = true;\n\t\tdiagram.Dispatcher.BeginInvoke(\n\t\t\tDispatcherPriority.Normal,\n\t\t\tnew System.Action(ArrangeDiagram));\n\t}\n\n\tvoid ArrangeDiagram()\n\t{\n\t\tArrangeNodes();\n\t\tArrangeLinks();\n\n\t\tarrangePending = false;\n\t}\n\n...<\/code><\/pre>\n\n\n\n<p>We will using two dictionaries to map the model objects to diagram objects:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DiagramNode GetView(SankeyNode node)\n{\n\tif (nodeViews.ContainsKey(node))\n\t\treturn nodeViews&#91;node];\n\n\tvar viewNode = diagram.Factory.\n\t\tCreateShapeNode(nodeRect, Shapes.Rectangle);\n\tviewNode.Brush = new SolidColorBrush(node.Color);\n\tviewNode.Stroke = Brushes.Transparent;\n\tnodeViews&#91;node] = viewNode;\n\treturn viewNode;\n}\nDictionary&lt;SankeyNode, DiagramNode&gt; nodeViews = new Dictionary&lt;SankeyNode, DiagramNode&gt;();\n\nDiagramLink GetView(SankeyFlow flow)\n{\n\tif (flowViews.ContainsKey(flow))\n\t\treturn flowViews&#91;flow];\n\n\tvar sourceNode = nodeViews&#91;flow.Source];\n\tvar targetNode = nodeViews&#91;flow.Target];\n\n\tvar viewLink = diagram.Factory.\n\t\tCreateDiagramLink(sourceNode, targetNode);\n\tviewLink.Shape = LinkShape.Bezier;\n\tviewLink.HeadShape = null;\n\n\tflowViews&#91;flow] = viewLink;\n\treturn viewLink;\n}\nDictionary&lt;SankeyFlow, DiagramLink&gt; flowViews = new Dictionary&lt;SankeyFlow, DiagramLink&gt;();\n<\/code><\/pre>\n\n\n\n<p>We can lay out simple diagrams by directly setting Bounds property of diagram nodes and ControlPoints of DiagramLinks:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void ArrangeNodes()\n{\n\tdouble x = margin;\n\tforeach (var column in model.Columns)\n\t{\n\t\tdouble y = margin;\n\n\t\tforeach (var node in column.Nodes)\n\t\t{\n\t\t\tnode.CalcFlow();\n\n\t\t\tvar diagramNode = GetView(node);\n\t\t\tdiagramNode.Move(x, y);\n\t\t\tdiagramNode.Resize(\n\t\t\t\tdiagramNode.Bounds.Width,\n\t\t\t\tnode.TotalFlow * flowUnitResolution);\n\t\t\ty += diagramNode.Bounds.Height + padding;\n\t\t}\n\n\t\tx += nodeRect.Width;\n\t\tx += columnDistance;\n\t}\n}\n\nvoid ArrangeLinks()\n{\n\tforeach (var column in model.Columns)\n\t{\n\t\tforeach (var node in column.Nodes)\n\t\t{\n\t\t\tvar diagramNode = GetView(node);\n\t\t\tvar y = diagramNode.Bounds.Y;\n\t\t\tforeach (var flow in node.OutFlows)\n\t\t\t{\n\t\t\t\tvar diagramLink = GetView(flow);\n\t\t\t\tvar thickness = flow.Amount * flowUnitResolution;\n\t\t\t\tdiagramLink.StrokeThickness = thickness;\n\n\t\t\t\ty += thickness \/ 2;\n\t\t\t\tvar startPoint = diagramLink.StartPoint;\n\t\t\t\tstartPoint.Y = y;\n\t\t\t\tdiagramLink.StartPoint = startPoint;\n\t\t\t\ty += thickness \/ 2;\n\n\t\t\t\tdiagramLink.Stroke = SemiTranparent(flow.Color);\n\t\t\t\tdiagramLink.HeadShape = null;\n\t\t\t}\n\t\t}\n\t}\n\n\tforeach (var column in model.Columns)\n\t{\n\t\tforeach (var node in column.Nodes)\n\t\t{\n\t\t\tvar diagramNode = GetView(node);\n\t\t\tvar y = diagramNode.Bounds.Y;\n\t\t\tforeach (var flow in node.InFlows)\n\t\t\t{\n\t\t\t\tvar diagramLink = GetView(flow);\n\n\t\t\t\tvar thickness = flow.Amount * flowUnitResolution;\n\t\t\t\ty += thickness \/ 2;\n\t\t\t\tvar endPoint = diagramLink.EndPoint;\n\t\t\t\tendPoint.Y = y;\n\t\t\t\tdiagramLink.EndPoint = endPoint;\n\t\t\t\ty += thickness \/ 2;\n\t\t\t}\n\t\t}\n\t}\n\n\tforeach (var link in diagram.Links)\n\t{\n\t\tvar p1 = link.ControlPoints&#91;1];\n\t\tp1.Y = link.StartPoint.Y;\n\t\tlink.ControlPoints&#91;1] = p1;\n\t\tvar p2 = link.ControlPoints&#91;2];\n\t\tp2.Y = link.EndPoint.Y;\n\t\tlink.ControlPoints&#91;2] = p2;\n\t\tlink.UpdateFromPoints();\n\t}\n}\n<\/code><\/pre>\n\n\n\n<p>For more complex diagrams, you could apply graph layout classes provided by MindFusion.Diagramming such as LayeredLayout or OrthogonalLayout.<\/p>\n\n\n\n<p>With adapter code in place, create a sample custom model in MainWindow:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public MainWindow()\n{\n\tInitializeComponent();\n\n\tmodel = new SankeyModel();\n\tadapter = new DiagramAdapter(model, diagram);\n\n\tPopulateModel();\n}\n\nvoid PopulateModel()\n{\n\tmodel.Columns.Add(new SankeyColumn());\n\tmodel.Columns.Add(new SankeyColumn());\n\tmodel.Columns.Add(new SankeyColumn());\n\n\tvar c0Node0 = new SankeyNode();\n\tmodel.Columns&#91;0].Nodes.Add(c0Node0);\n\n\tvar c1Node0 = new SankeyNode();\n\tvar c1Node1 = new SankeyNode();\n\tmodel.Columns&#91;1].Nodes.Add(c1Node0);\n\tmodel.Columns&#91;1].Nodes.Add(c1Node1);\n\n\tvar c2Node0 = new SankeyNode();\n\tvar c2Node1 = new SankeyNode();\n\tmodel.Columns&#91;2].Nodes.Add(c2Node0);\n\tmodel.Columns&#91;2].Nodes.Add(c2Node1);\n\n\tc0Node0.OutFlows.Add(new SankeyFlow { Amount = 5, Color = Colors.Red });\n\tc0Node0.OutFlows.Add(new SankeyFlow { Amount = 2, Color = Colors.Yellow });\n\tc0Node0.OutFlows.Add(new SankeyFlow { Amount = 4, Color = Colors.Blue });\n\tc0Node0.OutFlows.Add(new SankeyFlow { Amount = 3, Color = Colors.Magenta });\n\tc0Node0.OutFlows.Add(new SankeyFlow { Amount = 6, Color = Colors.Green });\n\n\tc1Node0.InFlows.Add(c0Node0.OutFlows&#91;0]);\n\tc1Node0.InFlows.Add(c0Node0.OutFlows&#91;1]);\n\tc1Node0.InFlows.Add(c0Node0.OutFlows&#91;3]);\n\n\tc1Node1.InFlows.Add(c0Node0.OutFlows&#91;2]);\n\tc1Node1.InFlows.Add(c0Node0.OutFlows&#91;4]);\n\n\tc1Node0.OutFlows.Add(new SankeyFlow { Amount = 3, Color = Colors.Goldenrod });\n\tc1Node0.OutFlows.Add(new SankeyFlow { Amount = 7, Color = Colors.Tomato });\n\tc1Node1.OutFlows.Add(new SankeyFlow { Amount = 5, Color = Colors.DeepSkyBlue });\n\tc1Node1.OutFlows.Add(new SankeyFlow { Amount = 5, Color = Colors.Bisque });\n\n\tc2Node0.InFlows.Add(c1Node0.OutFlows&#91;0]);\n\tc2Node1.InFlows.Add(c1Node1.OutFlows&#91;0]);\n\tc2Node1.InFlows.Add(c1Node0.OutFlows&#91;1]);\n\tc2Node1.InFlows.Add(c1Node1.OutFlows&#91;1]);\n}\n\nSankeyModel model;\nDiagramAdapter adapter;<\/code><\/pre>\n\n\n\n<p>This should display the diagram shown below:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/mindfusion.dev\/_samples\/sankey_diagram.png\" alt=\"\"\/><\/figure>\n\n\n\n<p>The complete sample project is available for download here:<br><a href=\"https:\/\/mindfusion.dev\/_samples\/SankeyDiagram.zip\" data-type=\"URL\" data-id=\"https:\/\/mindfusion.dev\/_samples\/SankeyDiagram.zip\">https:\/\/mindfusion.dev\/_samples\/SankeyDiagram.zip<\/a><\/p>\n\n\n\n<p>Enjoy!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this post we&#8217;ll show one possible way of using MindFusion WPF Diagram with custom model objects, and will create a simple Sankey diagram as an example.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false},"version":2}},"categories":[95,74],"tags":[3,678,58],"class_list":["post-2706","post","type-post","status-publish","format-standard","hentry","category-diagramming-2","category-sample-code","tag-diagram","tag-sankey","tag-wpf"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p3RlKs-HE","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/posts\/2706","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/comments?post=2706"}],"version-history":[{"count":2,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/posts\/2706\/revisions"}],"predecessor-version":[{"id":2709,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/posts\/2706\/revisions\/2709"}],"wp:attachment":[{"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/media?parent=2706"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/categories?post=2706"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mindfusion.dev\/blog\/wp-json\/wp\/v2\/tags?post=2706"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}