Hi,
Is it possible to generate Sankey diagrams with the WPF version of the library? If not is it a feature that you are planning to implement in the near future?
JS version looks great:
Thanks.
Hi,
Is it possible to generate Sankey diagrams with the WPF version of the library? If not is it a feature that you are planning to implement in the near future?
JS version looks great:
Thanks.
I think this should be quite possible. The implementation will be similar to what GoJS does. When I get a chance later today I can see if I can get a start on such a sample.
Thanks Walter, much appreciated.
Hey @walter did you get a chance to check this yet?
Sorry, I caught a cold and am not in a position to do significant things for a while until I get better.
oh ok, get well soon Walter.
Here you go. First the Sankey.xaml file:
<!-- Copyright © Northwoods Software Corporation, 2008-2018. All Rights Reserved. -->
<UserControl x:Class="Sankey.Sankey"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:go="http://schemas.nwoods.com/GoXam"
xmlns:local="clr-namespace:Sankey">
<UserControl.Resources>
<!-- for converting a color string to a Brush -->
<go:StringBrushConverter x:Key="theBrushConverter" />
<DataTemplate x:Key="NodeTemplate">
<StackPanel Orientation="Horizontal"
go:Node.LocationElementName="SHAPE"
go:Node.LocationSpot="MiddleLeft">
<TextBlock x:Name="LTEXT" Text="{Binding Path=Data.LText}" Margin="5"
FontWeight="Bold" FontSize="12pt" />
<Rectangle x:Name="SHAPE"
StrokeThickness="0"
go:Node.PortId=""
go:Node.FromSpot="RightSide"
go:Node.ToSpot="LeftSide"
Height="10"
Width="20"
Fill="{Binding Path=Data.Color, Converter={StaticResource theBrushConverter}}" />
<TextBlock x:Name="TEXT" Text="{Binding Path=Data.Text}" Margin="5"
FontWeight="Bold" FontSize="12pt" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="LinkTemplate">
<go:LinkPanel go:Part.LayerName="Background">
<go:Link.Route>
<go:Route Curve="Bezier" Adjusting="End"
FromEndSegmentLength="150" ToEndSegmentLength="150" />
</go:Link.Route>
<go:LinkShape Stroke="{Binding Path=Link.FromNode.Data.Color, Converter={StaticResource theBrushConverter}}"
Opacity="0.4"
StrokeThickness="{Binding Path=Data.Width}" />
</go:LinkPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<go:Diagram x:Name="myDiagram"
InitialStretch="UniformToFill"
NodeTemplate="{StaticResource NodeTemplate}"
LinkTemplate="{StaticResource LinkTemplate}">
<go:Diagram.Layout>
<local:SankeyLayout SetsPortSpots="False"
LayerSpacing="150"
ColumnSpacing="1" />
</go:Diagram.Layout>
</go:Diagram>
</Grid>
</UserControl>
And finally the Sankey.xaml.cs file:
/* Copyright © Northwoods Software Corporation, 2008-2018. All Rights Reserved. */
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Controls;
using Northwoods.GoXam;
using Northwoods.GoXam.Layout;
using Northwoods.GoXam.Model;
namespace Sankey {
public partial class Sankey : UserControl {
public Sankey() {
InitializeComponent();
var nodes = new ObservableCollection<SankeyNodeData>() {
new SankeyNodeData() { Key="Coal reserves", Text="Coal reserves", Color="#9d75c2" },
new SankeyNodeData() { Key="Coal imports", Text="Coal imports", Color="#9d75c2" },
new SankeyNodeData() { Key="Oil reserves", Text="Oil\nreserves", Color="#9d75c2" },
new SankeyNodeData() { Key="Oil imports", Text="Oil imports", Color="#9d75c2" },
new SankeyNodeData() { Key="Gas reserves", Text="Gas reserves", Color="#a1e194" },
new SankeyNodeData() { Key="Gas imports", Text="Gas imports", Color="#a1e194" },
new SankeyNodeData() { Key="UK land based bioenergy", Text="UK land based bioenergy", Color="#f6bcd5" },
new SankeyNodeData() { Key="Marine algae", Text="Marine algae", Color="#681313" },
new SankeyNodeData() { Key="Agricultural 'waste'", Text="Agricultural 'waste'", Color="#3483ba" },
new SankeyNodeData() { Key="Other waste", Text="Other waste", Color="#c9b7d8" },
new SankeyNodeData() { Key="Biomass imports", Text="Biomass imports", Color="#fea19f" },
new SankeyNodeData() { Key="Biofuel imports", Text="Biofuel imports", Color="#d93c3c" },
new SankeyNodeData() { Key="Coal", Text="Coal", Color="#9d75c2" },
new SankeyNodeData() { Key="Oil", Text="Oil", Color="#9d75c2" },
new SankeyNodeData() { Key="Natural gas", Text="Natural\ngas", Color="#a6dce6" },
new SankeyNodeData() { Key="Solar", Text="Solar", Color="#c9a59d" },
new SankeyNodeData() { Key="Solar PV", Text="Solar PV", Color="#c9a59d" },
new SankeyNodeData() { Key="Bio-conversion", Text="Bio-conversion", Color="#b5cbe9" },
new SankeyNodeData() { Key="Solid", Text="Solid", Color="#40a840" },
new SankeyNodeData() { Key="Liquid", Text="Liquid", Color="#fe8b25" },
new SankeyNodeData() { Key="Gas", Text="Gas", Color="#a1e194" },
new SankeyNodeData() { Key="Nuclear", Text="Nuclear", Color="#fea19f" },
new SankeyNodeData() { Key="Thermal generation", Text="Thermal\ngeneration", Color="#3483ba" },
new SankeyNodeData() { Key="CHP", Text="CHP", Color="yellow" },
new SankeyNodeData() { Key="Electricity imports", Text="Electricity imports", Color="yellow" },
new SankeyNodeData() { Key="Wind", Text="Wind", Color="#cbcbcb" },
new SankeyNodeData() { Key="Tidal", Text="Tidal", Color="#6f3a5f" },
new SankeyNodeData() { Key="Wave", Text="Wave", Color="#8b8b8b" },
new SankeyNodeData() { Key="Geothermal", Text="Geothermal", Color="#556171" },
new SankeyNodeData() { Key="Hydro", Text="Hydro", Color="#7c3e06" },
new SankeyNodeData() { Key="Electricity grid", Text="Electricity grid", Color="#e483c7" },
new SankeyNodeData() { Key="H2 conversion", Text="H2 conversion", Color="#868686" },
new SankeyNodeData() { Key="Solar Thermal", Text="Solar Thermal", Color="#c9a59d" },
new SankeyNodeData() { Key="H2", Text="H2", Color="#868686" },
new SankeyNodeData() { Key="Pumped heat", Text="Pumped heat", Color="#96665c" },
new SankeyNodeData() { Key="District heating", Text="District heating", Color="#c9b7d8" },
new SankeyNodeData() { Key="Losses", LText="Losses", Color="#fec184" },
new SankeyNodeData() { Key="Over generation / exports", LText="Over generation / exports", Color="#f6bcd5" },
new SankeyNodeData() { Key="Heating and cooling - homes", LText="Heating and cooling - homes", Color="#c7a39b" },
new SankeyNodeData() { Key="Road transport", LText="Road transport", Color="#cbcbcb" },
new SankeyNodeData() { Key="Heating and cooling - commercial", LText="Heating and cooling - commercial", Color="#c9a59d" },
new SankeyNodeData() { Key="Industry", LText="Industry", Color="#96665c" },
new SankeyNodeData() { Key="Lighting & appliances - homes", LText="Lighting & appliances - homes", Color="#2dc3d2" },
new SankeyNodeData() { Key="Lighting & appliances - commercial", LText="Lighting & appliances - commercial", Color="#2dc3d2" },
new SankeyNodeData() { Key="Agriculture", LText="Agriculture", Color="#5c5c10" },
new SankeyNodeData() { Key="Rail transport", LText="Rail transport", Color="#6b6b45" },
new SankeyNodeData() { Key="Domestic aviation", LText="Domestic aviation", Color="#40a840" },
new SankeyNodeData() { Key="National navigation", LText="National navigation", Color="#a1e194" },
new SankeyNodeData() { Key="International aviation", LText="International aviation", Color="#fec184" },
new SankeyNodeData() { Key="International shipping", LText="International shipping", Color="#fec184" },
new SankeyNodeData() { Key="Geosequestration", LText="Geosequestration", Color="#fec184" }
};
var links = new ObservableCollection<SankeyLinkData>() {
new SankeyLinkData() { From="Coal reserves", To="Coal", Width=31 },
new SankeyLinkData() { From="Coal imports", To="Coal", Width=86 },
new SankeyLinkData() { From="Oil reserves", To="Oil", Width=244 },
new SankeyLinkData() { From="Oil imports", To="Oil", Width=1 },
new SankeyLinkData() { From="Gas reserves", To="Natural gas", Width=182 },
new SankeyLinkData() { From="Gas imports", To="Natural gas", Width=61 },
new SankeyLinkData() { From="UK land based bioenergy", To="Bio-conversion", Width=1 },
new SankeyLinkData() { From="Marine algae", To="Bio-conversion", Width=1 },
new SankeyLinkData() { From="Agricultural 'waste'", To="Bio-conversion", Width=1 },
new SankeyLinkData() { From="Other waste", To="Bio-conversion", Width=8 },
new SankeyLinkData() { From="Other waste", To="Solid", Width=1 },
new SankeyLinkData() { From="Biomass imports", To="Solid", Width=1 },
new SankeyLinkData() { From="Biofuel imports", To="Liquid", Width=1 },
new SankeyLinkData() { From="Coal", To="Solid", Width=117 },
new SankeyLinkData() { From="Oil", To="Liquid", Width=244 },
new SankeyLinkData() { From="Natural gas", To="Gas", Width=244 },
new SankeyLinkData() { From="Solar", To="Solar PV", Width=1 },
new SankeyLinkData() { From="Solar PV", To="Electricity grid", Width=1 },
new SankeyLinkData() { From="Solar", To="Solar Thermal", Width=1 },
new SankeyLinkData() { From="Bio-conversion", To="Solid", Width=3 },
new SankeyLinkData() { From="Bio-conversion", To="Liquid", Width=1 },
new SankeyLinkData() { From="Bio-conversion", To="Gas", Width=5 },
new SankeyLinkData() { From="Bio-conversion", To="Losses", Width=1 },
new SankeyLinkData() { From="Solid", To="Over generation / exports", Width=1 },
new SankeyLinkData() { From="Liquid", To="Over generation / exports", Width=18 },
new SankeyLinkData() { From="Gas", To="Over generation / exports", Width=1 },
new SankeyLinkData() { From="Solid", To="Thermal generation", Width=106 },
new SankeyLinkData() { From="Liquid", To="Thermal generation", Width=2 },
new SankeyLinkData() { From="Gas", To="Thermal generation", Width=87 },
new SankeyLinkData() { From="Nuclear", To="Thermal generation", Width=41 },
new SankeyLinkData() { From="Thermal generation", To="District heating", Width=2 },
new SankeyLinkData() { From="Thermal generation", To="Electricity grid", Width=92 },
new SankeyLinkData() { From="Thermal generation", To="Losses", Width=142 },
new SankeyLinkData() { From="Solid", To="CHP", Width=1 },
new SankeyLinkData() { From="Liquid", To="CHP", Width=1 },
new SankeyLinkData() { From="Gas", To="CHP", Width=1 },
new SankeyLinkData() { From="CHP", To="Electricity grid", Width=1 },
new SankeyLinkData() { From="CHP", To="Losses", Width=1 },
new SankeyLinkData() { From="Electricity imports", To="Electricity grid", Width=1 },
new SankeyLinkData() { From="Wind", To="Electricity grid", Width=1 },
new SankeyLinkData() { From="Tidal", To="Electricity grid", Width=1 },
new SankeyLinkData() { From="Wave", To="Electricity grid", Width=1 },
new SankeyLinkData() { From="Geothermal", To="Electricity grid", Width=1 },
new SankeyLinkData() { From="Hydro", To="Electricity grid", Width=1 },
new SankeyLinkData() { From="Electricity grid", To="H2 conversion", Width=1 },
new SankeyLinkData() { From="Electricity grid", To="Over generation / exports", Width=1 },
new SankeyLinkData() { From="Electricity grid", To="Losses", Width=6 },
new SankeyLinkData() { From="Gas", To="H2 conversion", Width=1 },
new SankeyLinkData() { From="H2 conversion", To="H2", Width=1 },
new SankeyLinkData() { From="H2 conversion", To="Losses", Width=1 },
new SankeyLinkData() { From="Solar Thermal", To="Heating and cooling - homes", Width=1 },
new SankeyLinkData() { From="H2", To="Road transport", Width=1 },
new SankeyLinkData() { From="Pumped heat", To="Heating and cooling - homes", Width=1 },
new SankeyLinkData() { From="Pumped heat", To="Heating and cooling - commercial", Width=1 },
new SankeyLinkData() { From="CHP", To="Heating and cooling - homes", Width=1 },
new SankeyLinkData() { From="CHP", To="Heating and cooling - commercial", Width=1 },
new SankeyLinkData() { From="District heating", To="Heating and cooling - homes", Width=1 },
new SankeyLinkData() { From="District heating", To="Heating and cooling - commercial", Width=1 },
new SankeyLinkData() { From="District heating", To="Industry", Width=2 },
new SankeyLinkData() { From="Electricity grid", To="Heating and cooling - homes", Width=7 },
new SankeyLinkData() { From="Solid", To="Heating and cooling - homes", Width=3 },
new SankeyLinkData() { From="Liquid", To="Heating and cooling - homes", Width=3 },
new SankeyLinkData() { From="Gas", To="Heating and cooling - homes", Width=81 },
new SankeyLinkData() { From="Electricity grid", To="Heating and cooling - commercial", Width=7 },
new SankeyLinkData() { From="Solid", To="Heating and cooling - commercial", Width=1 },
new SankeyLinkData() { From="Liquid", To="Heating and cooling - commercial", Width=2 },
new SankeyLinkData() { From="Gas", To="Heating and cooling - commercial", Width=19 },
new SankeyLinkData() { From="Electricity grid", To="Lighting & appliances - homes", Width=21 },
new SankeyLinkData() { From="Gas", To="Lighting & appliances - homes", Width=2 },
new SankeyLinkData() { From="Electricity grid", To="Lighting & appliances - commercial", Width=18 },
new SankeyLinkData() { From="Gas", To="Lighting & appliances - commercial", Width=2 },
new SankeyLinkData() { From="Electricity grid", To="Industry", Width=30 },
new SankeyLinkData() { From="Solid", To="Industry", Width=13 },
new SankeyLinkData() { From="Liquid", To="Industry", Width=34 },
new SankeyLinkData() { From="Gas", To="Industry", Width=54 },
new SankeyLinkData() { From="Electricity grid", To="Agriculture", Width=1 },
new SankeyLinkData() { From="Solid", To="Agriculture", Width=1 },
new SankeyLinkData() { From="Liquid", To="Agriculture", Width=1 },
new SankeyLinkData() { From="Gas", To="Agriculture", Width=1 },
new SankeyLinkData() { From="Electricity grid", To="Road transport", Width=1 },
new SankeyLinkData() { From="Liquid", To="Road transport", Width=122 },
new SankeyLinkData() { From="Electricity grid", To="Rail transport", Width=2 },
new SankeyLinkData() { From="Liquid", To="Rail transport", Width=1 },
new SankeyLinkData() { From="Liquid", To="Domestic aviation", Width=2 },
new SankeyLinkData() { From="Liquid", To="National navigation", Width=4 },
new SankeyLinkData() { From="Liquid", To="International aviation", Width=38 },
new SankeyLinkData() { From="Liquid", To="International shipping", Width=13 },
new SankeyLinkData() { From="Electricity grid", To="Geosequestration", Width=1 },
new SankeyLinkData() { From="Gas", To="Losses", Width=2 }
};
var model = new GraphLinksModel<SankeyNodeData, String, String, SankeyLinkData>();
model.NodesSource = nodes;
model.LinksSource = links;
myDiagram.Model = model;
}
}
public class SankeyNodeData : GraphLinksModelNodeData<String> {
public String LText { get; set; }
public String Color { get; set; }
}
public class SankeyLinkData : GraphLinksModelLinkData<String, String> {
public double Width { get; set; }
}
public class SankeyLayout : LayeredDigraphLayout {
public SankeyLayout() {
this.PackOption = LayeredDigraphPack.Straighten | LayeredDigraphPack.Median;
}
// Before creating the LayeredDigraphNetwork of vertexes and edges,
// determine the desired height of each node (Shape).
public override LayeredDigraphNetwork CreateNetwork() {
foreach (var node in this.Diagram.Nodes) {
var height = getAutoHeightForNode(node);
var fontsize = Math.Max(12, Math.Round(height / 8));
var shape = node.FindNamedDescendant("SHAPE");
var text = node.FindNamedDescendant("TEXT") as TextBlock;
var ltext = node.FindNamedDescendant("LTEXT") as TextBlock;
if (shape != null) shape.Height = height;
if (text != null) text.FontSize = fontsize;
if (ltext != null) ltext.FontSize = fontsize;
node.Remeasure();
}
return base.CreateNetwork();
}
double getAutoHeightForNode(Node node) {
var heightIn = 0.0;
foreach (var link in node.LinksInto) heightIn += ComputeThickness(link);
var heightOut = 0.0;
foreach (var link in node.LinksOutOf) heightOut += ComputeThickness(link);
var h = Math.Max(heightIn, heightOut);
if (h < 10) h = 10;
return h;
}
public static double ComputeThickness(Link link) {
if (link == null) return 1;
var data = link.Data as SankeyLinkData;
if (data != null) return data.Width;
return 1;
}
// treat dummy vertexes as having the thickness of the link that they are in
protected override int NodeMinColumnSpace(LayeredDigraphVertex v, bool topleft) {
if (v.Node == null) {
if (v.EdgesCount >= 1) {
var max = 1.0;
foreach (var edge in v.Edges) {
if (edge.Link != null) {
var t = ComputeThickness(edge.Link);
if (t > max) max = t;
break;
}
}
return (int)Math.Ceiling(max/this.ColumnSpacing);
}
return 1;
}
return base.NodeMinColumnSpace(v, topleft);
}
protected override void AssignLayers() {
base.AssignLayers();
var maxlayer = this.MaxLayer;
// now make sure every vertex with no outputs is maxlayer
foreach (var v in this.Network.Vertexes) {
if (v.DestinationVertexes.Count() == 0) {
v.Layer = 0;
}
if (v.SourceVertexes.Count() == 0) {
v.Layer = maxlayer;
}
}
}
protected override void LayoutLinks() {
base.LayoutLinks();
// need to adjust link.Route.ToSpot and .FromSpot so that the links
// are stacked on top each other based on their computed thicknesses
foreach (var v in this.Network.Vertexes) {
if (v.Node == null) continue;
var links = v.Node.LinksInto.OrderBy(l => l.Route.Points[l.Route.PointsCount-1].Y).ToList();
var total = 0.0;
foreach (var l in links) total += ComputeThickness(l);
var y = -total/2;
foreach (var l in links) {
var t = ComputeThickness(l);
l.Route.ToSpot = new Spot(0, 0.5, 0, y + t/2);
y += t;
}
links = v.Node.LinksOutOf.OrderBy(l => l.Route.Points[l.Route.PointsCount-1].Y).ToList();
total = 0.0;
foreach (var l in links) total += ComputeThickness(l);
y = -total/2;
foreach (var l in links) {
var t = ComputeThickness(l);
l.Route.FromSpot = new Spot(1, 0.5, 0, y + t/2);
y += t;
}
}
foreach (var e in this.Network.Edges) {
if (e.Link == null) continue;
e.Link.Route.InvalidateRoute(); // let Route.Adjusting keep the intermediate points
}
}
}
// end of SankeyLayout
}
The result:
Awesome! Thanks @walter