Reflexive link doesn't display correctly in Sankey Diagram

I’m trying to build my own Sankey diagram based on the sample at https://gojs.net/latest/samples/sankey.html. However, I can’t get reflexive links to display correctly. I added a reflexive link to the Oil node and it looks like this:
1-oil-to-oil-no-customization

I would like the link to exit the node on the right-hand side, wrap around the node, and enter on the left-hand side. I added my own input port on the left and output port on the right (red) and this is what it looks like:
2-oil-to-oil-custom-ports

The link is exiting the right and entering the left but its not wrapping around the node. I changed the routing for reflexive links to be orthogonal and this is what it looks like:
3-oil-to-oil-custom-routing

This is close to what I want but the wrapping doesn’t work if the node text is long. I lengthened the text of the Oil node and this is what it looks like:

Now the link isn’t wrapping around the node anymore.

Does anybody know how to get the link to wrap around the node? I’m using GoJS 2.1. Thank you

Here are my code changes for the sample:

// Before creating the LayeredDigraphNetwork of vertexes and edges,
// determine the desired height of each node (Shape).
SankeyLayout.prototype.createNetwork = function () {
  this.diagram.nodes.each(function (node) {
    var height = getAutoHeightForNode(node);
    var font = "bold " + Math.max(12, Math.round(height / 8)) + "pt Segoe UI, sans-serif"
    var shape = node.findObject("SHAPE");        
    var text = node.findObject("TEXT");
    var ltext = node.findObject("LTEXT");
    if (shape) shape.height = height;        
    if (text) text.font = font;
    if (ltext) ltext.font = font;

    // *** I added this to update the heights of the ports to match the main shape.
    var input = node.findObject("INPUT");
    var output = node.findObject("OUTPUT");
    if (input) input.height = height;
    if (output) output.height = height;
  });
  return go.LayeredDigraphLayout.prototype.createNetwork.call(this);
};

// define the Node template
myDiagram.nodeTemplate =
  $(go.Node, go.Panel.Horizontal,
    {
      locationObjectName: "SHAPE",
      locationSpot: go.Spot.MiddleLeft,
      portSpreading: go.Node.SpreadingPacked  // rather than the default go.Node.SpreadingEvenly
    },
    $(go.TextBlock, textStyle(),
      { name: "LTEXT" },
      new go.Binding("text", "ltext")),
    // *** My custom input port
    $(go.Shape,
      {
        name: "INPUT",
        fill: "red",
        strokeWidth: 0,
        portId: "in",
        toSpot: go.Spot.LeftSide,
        height: 50,
        width: 5
      }),
    $(go.Shape,
      {
        name: "SHAPE",
        figure: "Rectangle",
        fill: "#2E8DEF",  // default fill color
        stroke: null,
        strokeWidth: 0,
        portId: "",
        fromSpot: go.Spot.RightSide,
        toSpot: go.Spot.LeftSide,
        height: 50,
        width: 20
      },
      new go.Binding("fill", "color")),
    // *** My custom output port
    $(go.Shape,
      {
        name: "OUTPUT",
        fill: "red",
        strokeWidth: 0,
        portId: "out",
        fromSpot: go.Spot.RightSide,
        height: 50,
        width: 5
      }),
    $(go.TextBlock, textStyle(),
      { name: "TEXT" },
      new go.Binding("text"))
  );

myDiagram.linkTemplate =
  $(go.Link, go.Link.Bezier,
    {
      selectionAdornmentTemplate: linkSelectionAdornmentTemplate,
      layerName: "Background",
      fromEndSegmentLength: 150, toEndSegmentLength: 150,
      adjusting: go.Link.End,
      // *** My custom ports
      fromPortId: 'out',
      toPortId: 'in'
    },
    // *** Change the routing for reflexive links
    new go.Binding("routing", "", function (linkData) {
      if (linkData.from === linkData.to) {
        return go.Link.Orthogonal;
      }

      return go.Link.None;
    }),
    $(go.Shape, { strokeWidth: 4, stroke: "rgba(173, 173, 173, 0.25)" },
      new go.Binding("stroke", "", getAutoLinkColor),
      new go.Binding("strokeWidth", "width"))
  );

I think it might be easier to just define a subclass of Link that overrides Link.computePoints. A simple Bezier curve cannot do what I think you want.

    function SankeyLink() {
      go.Link.call(this);
    }
    go.Diagram.inherit(SankeyLink, go.Link);

    SankeyLink.prototype.computePoints = function() {
      var node = this.fromNode;
      if (node === this.toNode) {
        var b = node.port.getDocumentBounds();
        this.clearPoints();
        this.addPoint(new go.Point(b.right, b.bottom));
        this.addPoint(new go.Point(b.right + 50, b.bottom));
        this.addPoint(new go.Point(b.right + 50, b.bottom + 50));
        this.addPoint(new go.Point(b.centerX, b.bottom + 50));
        this.addPoint(new go.Point(b.left - 50, b.bottom + 50));
        this.addPoint(new go.Point(b.left - 50, b.bottom));
        this.addPoint(new go.Point(b.left, b.bottom));
        return true;
      }
      return go.Link.prototype.computePoints.call(this);
    }

And replace go.Link with SankeyLink in your link template. None of the Bindings that you added to the link template are needed, and no extra ports are needed on the node template.

Alas, it appears that all of the links are centered on the sides of the port, so half of the strokeWidth of the reflexive link is off the end of the node. I’m not sure about the solution for that. I thought you’d at least want right now the easy solution to what you were working on.

Thank you Walter, that did the trick! I made some changes to your code to consider the link’s thickness when drawing the reflexive link:

SankeyLink.prototype.computePoints = function () {
  var node = this.fromNode;
  if (node === this.toNode) {
    var b = node.port.getDocumentBounds();
    var lineThickness = this.computeThickness();
    var y = b.bottom - (lineThickness / 2);
    var minOffset = 20;
    var offset = lineThickness * 1.5;
    if (offset < minOffset) {
      offset = minOffset;
    }

    this.clearPoints();
    this.addPoint(new go.Point(b.right, y));
    this.addPoint(new go.Point(b.right + offset, y));
    this.addPoint(new go.Point(b.right + offset, y + offset));
    this.addPoint(new go.Point(b.centerX, y + offset));
    this.addPoint(new go.Point(b.left - offset, y + offset));
    this.addPoint(new go.Point(b.left - offset, y));
    this.addPoint(new go.Point(b.left, y));

    return true;
  }

  return go.Link.prototype.computePoints.call(this);
}

Now it looks like this:
Capture

The reflexive link looks correct but the other links are not spread evenly across the sides of the node. Do you have any ideas how to solve this? Thanks

I’ll see if I can figure out an answer later today. But it might not be easy.

Here you go. I also spent some time improving the layout overall, as well as handling multiple reflexive links. It uses a second Shape which is not a port but which acts like one when reflexive Links use the SankeyLink class. Here it is with four reflexive links each with different thicknesses.
image

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Sankey Diagram with Reflexive Flows</title>
  <meta name="description" content="A Sankey diagram with the thickness of links indicating the flow quantity." />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
  <script src="go.js"></script>
  <script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
    function SankeyLayout() {
      go.LayeredDigraphLayout.call(this);
    }
    go.Diagram.inherit(SankeyLayout, go.LayeredDigraphLayout);

    // determine the desired height of each node/vertex,
    // based on the thicknesses of the connected links;
    // actually modify the height of each node's SHAPE
    SankeyLayout.prototype.makeNetwork = function(coll) {
      var net = go.LayeredDigraphLayout.prototype.makeNetwork.call(this, coll);
      this.diagram.nodes.each(function(node) {
        var heights = getAutoHeightForNode(node);
        var height = heights[0];
        var both = heights[1];
        if (height > 0 || both > 0) {
          var font = "bold " + Math.max(12, Math.round(height / 8)) + "pt Segoe UI, sans-serif"
          var shape = node.findObject("SHAPE");
          var shape2 = node.findObject("SHAPE2");
          var text = node.findObject("TEXT");
          var ltext = node.findObject("LTEXT");
          if (shape) shape.height = height;
          if (shape2) shape2.height = both;
          if (text) text.font = font;
          if (ltext) ltext.font = font;
          node.ensureBounds();
        }
        var v = net.findVertex(node);
        if (v !== null) {
          var r = node.actualBounds;
          v.width = r.width;
          v.height = r.height + ((both > 0) ? (both + 10) : 0);
          v.focusY = v.height/2;
        }
      });
      return net;
    };

    function getAutoHeightForNode(node) {
      var selves = new go.Set();  // remember reflexive links
      var heightIn = 0;
      var it = node.findLinksInto()
      while (it.next()) {
        var link = it.value;
        if (link.fromNode !== link.toNode) {
          heightIn += link.computeThickness();
        } else {
          selves.add(link);
        }
      }
      var heightOut = 0;
      var it = node.findLinksOutOf()
      while (it.next()) {
        var link = it.value;
        if (link.fromNode !== link.toNode) {
          heightOut += link.computeThickness();
        }
      }
      var h = Math.max(heightIn, heightOut);
      if (h < 10) h = 10;
      var heightBoth = 0;  // compute total height of reflexive links
      selves.each(function(link) {
        var sw = link.computeThickness();
        heightBoth += sw;
      });
      var th = heightBoth;  // assign curviness for SankeyLink.computePoints
      selves.each(function(link) {
        var sw = link.computeThickness();
        link.curviness = th;
        th -= sw;
      });
      return [h, heightBoth];
    };

    // treat dummy vertexes as having the thickness of the link that they are in
    SankeyLayout.prototype.nodeMinColumnSpace = function(v, topleft) {
      if (v.node === null) {
        if (v.edgesCount >= 1) {
          var max = 1;
          var it = v.edges;
          while (it.next()) {
            var edge = it.value;
            if (edge.link != null) {
              var t = edge.link.computeThickness();
              if (t > max) max = t;
              break;
            }
          }
          return Math.max(2, Math.ceil(max / this.columnSpacing));
        }
        return 2;
      }
      return go.LayeredDigraphLayout.prototype.nodeMinColumnSpace.call(this, v, topleft);
    }

    // treat dummy vertexes as being thicker, so that the Bezier curves are gentler
    SankeyLayout.prototype.nodeMinLayerSpace = function(v, topleft) {
      if (v.node === null) return 100;
      return go.LayeredDigraphLayout.prototype.nodeMinLayerSpace.call(this, v, topleft);
    }

    SankeyLayout.prototype.assignLayers = function() {
      go.LayeredDigraphLayout.prototype.assignLayers.call(this);
      var maxlayer = this.maxLayer;
      // now make sure every vertex with no outputs is maxlayer
      for (var it = this.network.vertexes.iterator; it.next();) {
        var v = it.value;
        var node = v.node;
        if (v.destinationVertexes.count == 0) {
          v.layer = 0;
        }
        if (v.sourceVertexes.count == 0) {
          v.layer = maxlayer;
        }
      }
      // from now on, the LayeredDigraphLayout will think that the Node is bigger than it really is
      // (other than the ones that are the widest or tallest in their respective layer).
    };

    SankeyLayout.prototype.commitLayout = function() {
      go.LayeredDigraphLayout.prototype.commitLayout.call(this);
      for (var it = this.network.edges.iterator; it.next();) {
        var link = it.value.link;
        if (link && link.curve === go.Link.Bezier) {
          // depend on Link.adjusting === go.Link.End to fix up the end points of the links
          // without losing the intermediate points of the route as determined by LayeredDigraphLayout
          link.invalidateRoute();
        }
      }
    };
    // end of SankeyLayout

    // Implement a custom Link class just to route reflexive links appropriately.
    // If your graphs will not have links that connect a node with itself, you do not need this class.
    function SankeyLink() {
      go.Link.call(this);
    }
    go.Diagram.inherit(SankeyLink, go.Link);

    SankeyLink.prototype.computePoints = function() {
      var node = this.fromNode;
      if (node === this.toNode) {
        // special routing of reflexive links
        var p2 = node.findObject("SHAPE2");
        if (p2 === null) return false;
        var r = p2.getDocumentBounds();
        var sw = this.computeThickness();
        var curve = isNaN(this.curviness) ? 0 : this.curviness;
        var t = r.bottom - curve + sw/2;
        var gap = 10;
        var e = curve*2 - sw;
        var b = r.bottom + gap + curve - sw/2;
        this.clearPoints();
        this.addPoint(new go.Point(r.right, t));
        this.addPoint(new go.Point(r.right + e, t));
        this.addPoint(new go.Point(r.right + e, b));
        this.addPoint(new go.Point(r.centerX, b));
        this.addPoint(new go.Point(r.left - e, b));
        this.addPoint(new go.Point(r.left - e, t));
        this.addPoint(new go.Point(r.left, t));
        return true;
      }
      return go.Link.prototype.computePoints.call(this);
    }

    function init() {
      if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
      var $ = go.GraphObject.make;  // for conciseness in defining templates

      myDiagram =
        $(go.Diagram, "myDiagramDiv", // the ID of the DIV HTML element
          {
            initialAutoScale: go.Diagram.UniformToFill,
            "animationManager.isEnabled": false,
            layout: $(SankeyLayout,
              {
                setsPortSpots: false,  // to allow the "Side" spots on the nodes to take effect
                direction: 0,  // rightwards
                layeringOption: go.LayeredDigraphLayout.LayerOptimalLinkLength,
                packOption: go.LayeredDigraphLayout.PackStraighten || go.LayeredDigraphLayout.PackMedian,
                layerSpacing: 100,  // lots of space between layers, for nicer thick links
                columnSpacing: 1
              })
          });

      var colors = ["#AC193D/#BF1E4B", "#2672EC/#2E8DEF", "#8C0095/#A700AE", "#5133AB/#643EBF", "#008299/#00A0B1", "#D24726/#DC572E", "#008A00/#00A600", "#094AB2/#0A5BC4"];

      // this function provides a common style for the TextBlocks
      function textStyle() {
        return { font: "bold 12pt Segoe UI, sans-serif", stroke: "black", margin: 5 };
      }

      // define the Node template
      myDiagram.nodeTemplate =
        $(go.Node, "Horizontal",
          {
            locationObjectName: "BAR",
            locationSpot: go.Spot.MiddleLeft,
            portSpreading: go.Node.SpreadingPacked  // rather than the default go.Node.SpreadingEvenly
          },
          $(go.TextBlock, textStyle(),
            { name: "LTEXT" },
            new go.Binding("text", "ltext")),
          $(go.Panel, "Vertical",
            { name: "BAR" },
            $(go.Shape,
              {
                name: "SHAPE",
                fill: "#2E8DEF",  // default fill color
                strokeWidth: 0,
                portId: "",
                fromSpot: go.Spot.RightSide,
                toSpot: go.Spot.LeftSide,
                height: 10,
                width: 20
              },
              new go.Binding("fill", "color")),
            $(go.Shape,
              {
                name: "SHAPE2",
                fill: "#2E8DEF",  // default fill color
                strokeWidth: 0,
                height: 0,
                width: 20
              },
              new go.Binding("fill", "color"))
          ),
          $(go.TextBlock, textStyle(),
            { name: "TEXT" },
            new go.Binding("text"))
        );

      function getAutoLinkColor(data) {
        var nodedata = myDiagram.model.findNodeDataForKey(data.from);
        var hex = nodedata.color;
        if (hex.charAt(0) == '#') {
          var rgb = parseInt(hex.substr(1, 6), 16);
          var r = rgb >> 16;
          var g = rgb >> 8 & 0xFF;
          var b = rgb & 0xFF;
          var alpha = 0.4;
          if (data.width <= 2) alpha = 1;
          var rgba = "rgba(" + r + "," + g + "," + b + ", " + alpha + ")";
          return rgba;
        }
        return "rgba(173, 173, 173, 0.25)";
      }

      // define the Link template
      var linkSelectionAdornmentTemplate =
        $(go.Adornment, "Link",
          $(go.Shape,
            { isPanelMain: true, fill: null, stroke: "rgba(0, 0, 255, 0.3)", strokeWidth: 0 })  // use selection object's strokeWidth
        );

      myDiagram.linkTemplate =
        $(SankeyLink, go.Link.Bezier,
          {
            selectionAdornmentTemplate: linkSelectionAdornmentTemplate,
            layerName: "Background",
            fromEndSegmentLength: 150, toEndSegmentLength: 150,
            adjusting: go.Link.End
          },
          $(go.Shape, { strokeWidth: 4, stroke: "rgba(173, 173, 173, 0.25)" },
            new go.Binding("stroke", "", getAutoLinkColor),
            new go.Binding("strokeWidth", "width"))
        );

      // read in the JSON-format data from the "mySavedModel" element
      load();
    }

    function load() {
      myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
    }
  </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; width: 99%; height: 850px"></div>
  <p>
    A Sankey diagram is a type of flow diagram where the Link thickness is proportional to the flow quantity.
  </p>
  <div>
    <div>
      <button onclick="load()">Read</button>
    </div>
    <textarea id="mySavedModel" style="width:100%;height:250px">
{ "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":"Coal reserves", "text":"Coal reserves", "color":"#9d75c2"},
{"key":"Coal imports", "text":"Coal imports", "color":"#9d75c2"},
{"key":"Oil reserves", "text":"Oil\nreserves", "color":"#9d75c2"},
{"key":"Oil imports", "text":"Oil imports", "color":"#9d75c2"},
{"key":"Gas reserves", "text":"Gas reserves", "color":"#a1e194"},
{"key":"Gas imports", "text":"Gas imports", "color":"#a1e194"},
{"key":"UK land based bioenergy", "text":"UK land based bioenergy", "color":"#f6bcd5"},
{"key":"Marine algae", "text":"Marine algae", "color":"#681313"},
{"key":"Agricultural 'waste'", "text":"Agricultural 'waste'", "color":"#3483ba"},
{"key":"Other waste", "text":"Other waste", "color":"#c9b7d8"},
{"key":"Biomass imports", "text":"Biomass imports", "color":"#fea19f"},
{"key":"Biofuel imports", "text":"Biofuel imports", "color":"#d93c3c"},
{"key":"Coal", "text":"Coal", "color":"#9d75c2"},
{"key":"Oil", "text":"Oil", "color":"#9d75c2"},
{"key":"Natural gas", "text":"Natural\ngas", "color":"#a6dce6"},
{"key":"Solar", "text":"Solar", "color":"#c9a59d"},
{"key":"Solar PV", "text":"Solar PV", "color":"#c9a59d"},
{"key":"Bio-conversion", "text":"Bio-conversion", "color":"#b5cbe9"},
{"key":"Solid", "text":"Solid", "color":"#40a840"},
{"key":"Liquid", "text":"Liquid", "color":"#fe8b25"},
{"key":"Gas", "text":"Gas", "color":"#a1e194"},
{"key":"Nuclear", "text":"Nuclear", "color":"#fea19f"},
{"key":"Thermal generation", "text":"Thermal\ngeneration", "color":"#3483ba"},
{"key":"CHP", "text":"CHP", "color":"yellow"},
{"key":"Electricity imports", "text":"Electricity imports", "color":"yellow"},
{"key":"Wind", "text":"Wind", "color":"#cbcbcb"},
{"key":"Tidal", "text":"Tidal", "color":"#6f3a5f"},
{"key":"Wave", "text":"Wave", "color":"#8b8b8b"},
{"key":"Geothermal", "text":"Geothermal", "color":"#556171"},
{"key":"Hydro", "text":"Hydro", "color":"#7c3e06"},
{"key":"Electricity grid", "text":"Electricity grid", "color":"#e483c7"},
{"key":"H2 conversion", "text":"H2 conversion", "color":"#868686"},
{"key":"Solar Thermal", "text":"Solar Thermal", "color":"#c9a59d"},
{"key":"H2", "text":"H2", "color":"#868686"},
{"key":"Pumped heat", "text":"Pumped heat", "color":"#96665c"},
{"key":"District heating", "text":"District heating", "color":"#c9b7d8"},
{"key":"Losses", "ltext":"Losses", "color":"#fec184"},
{"key":"Over generation / exports", "ltext":"Over generation / exports", "color":"#f6bcd5"},
{"key":"Heating and cooling - homes", "ltext":"Heating and cooling - homes", "color":"#c7a39b"},
{"key":"Road transport", "ltext":"Road transport", "color":"#cbcbcb"},
{"key":"Heating and cooling - commercial", "ltext":"Heating and cooling - commercial", "color":"#c9a59d"},
{"key":"Industry", "ltext":"Industry", "color":"#96665c"},
{"key":"Lighting & appliances - homes", "ltext":"Lighting & appliances - homes", "color":"#2dc3d2"},
{"key":"Lighting & appliances - commercial", "ltext":"Lighting & appliances - commercial", "color":"#2dc3d2"},
{"key":"Agriculture", "ltext":"Agriculture", "color":"#5c5c10"},
{"key":"Rail transport", "ltext":"Rail transport", "color":"#6b6b45"},
{"key":"Domestic aviation", "ltext":"Domestic aviation", "color":"#40a840"},
{"key":"National navigation", "ltext":"National navigation", "color":"#a1e194"},
{"key":"International aviation", "ltext":"International aviation", "color":"#fec184"},
{"key":"International shipping", "ltext":"International shipping", "color":"#fec184"},
{"key":"Geosequestration", "ltext":"Geosequestration", "color":"#fec184"}
    ], "linkDataArray": [
{"from":"Coal reserves", "to":"Coal", "width":31},
{"from":"Coal imports", "to":"Coal", "width":86},
{"from":"Coal", "to":"Coal", "width":2},
{"from":"Coal", "to":"Coal", "width":36},
{"from":"Coal", "to":"Coal", "width":16},
{"from":"Coal", "to":"Coal", "width":6},
{"from":"Oil reserves", "to":"Oil", "width":244},
{"from":"Oil imports", "to":"Oil", "width":1},
{"from":"Gas reserves", "to":"Natural gas", "width":182},
{"from":"Gas imports", "to":"Natural gas", "width":61},
{"from":"UK land based bioenergy", "to":"Bio-conversion", "width":1},
{"from":"Marine algae", "to":"Bio-conversion", "width":1},
{"from":"Agricultural 'waste'", "to":"Bio-conversion", "width":1},
{"from":"Other waste", "to":"Bio-conversion", "width":8},
{"from":"Other waste", "to":"Solid", "width":1},
{"from":"Biomass imports", "to":"Solid", "width":1},
{"from":"Biofuel imports", "to":"Liquid", "width":1},
{"from":"Coal", "to":"Solid", "width":117},
{"from":"Oil", "to":"Liquid", "width":244},
{"from":"Natural gas", "to":"Gas", "width":244},
{"from":"Solar", "to":"Solar PV", "width":1},
{"from":"Solar PV", "to":"Electricity grid", "width":1},
{"from":"Solar", "to":"Solar Thermal", "width":1},
{"from":"Bio-conversion", "to":"Solid", "width":3},
{"from":"Bio-conversion", "to":"Liquid", "width":1},
{"from":"Bio-conversion", "to":"Gas", "width":5},
{"from":"Bio-conversion", "to":"Losses", "width":1},
{"from":"Solid", "to":"Over generation / exports", "width":1},
{"from":"Liquid", "to":"Over generation / exports", "width":18},
{"from":"Gas", "to":"Over generation / exports", "width":1},
{"from":"Solid", "to":"Thermal generation", "width":106},
{"from":"Liquid", "to":"Thermal generation", "width":2},
{"from":"Gas", "to":"Thermal generation", "width":87},
{"from":"Nuclear", "to":"Thermal generation", "width":41},
{"from":"Thermal generation", "to":"District heating", "width":2},
{"from":"Thermal generation", "to":"Electricity grid", "width":92},
{"from":"Thermal generation", "to":"Losses", "width":142},
{"from":"Solid", "to":"CHP", "width":1},
{"from":"Liquid", "to":"CHP", "width":1},
{"from":"Gas", "to":"CHP", "width":1},
{"from":"CHP", "to":"Electricity grid", "width":1},
{"from":"CHP", "to":"Losses", "width":1},
{"from":"Electricity imports", "to":"Electricity grid", "width":1},
{"from":"Wind", "to":"Electricity grid", "width":1},
{"from":"Tidal", "to":"Electricity grid", "width":1},
{"from":"Wave", "to":"Electricity grid", "width":1},
{"from":"Geothermal", "to":"Electricity grid", "width":1},
{"from":"Hydro", "to":"Electricity grid", "width":1},
{"from":"Electricity grid", "to":"H2 conversion", "width":1},
{"from":"Electricity grid", "to":"Over generation / exports", "width":1},
{"from":"Electricity grid", "to":"Losses", "width":6},
{"from":"Gas", "to":"H2 conversion", "width":1},
{"from":"H2 conversion", "to":"H2", "width":1},
{"from":"H2 conversion", "to":"Losses", "width":1},
{"from":"Solar Thermal", "to":"Heating and cooling - homes", "width":1},
{"from":"H2", "to":"Road transport", "width":1},
{"from":"Pumped heat", "to":"Heating and cooling - homes", "width":1},
{"from":"Pumped heat", "to":"Heating and cooling - commercial", "width":1},
{"from":"CHP", "to":"Heating and cooling - homes", "width":1},
{"from":"CHP", "to":"Heating and cooling - commercial", "width":1},
{"from":"District heating", "to":"Heating and cooling - homes", "width":1},
{"from":"District heating", "to":"Heating and cooling - commercial", "width":1},
{"from":"District heating", "to":"Industry", "width":2},
{"from":"Electricity grid", "to":"Heating and cooling - homes", "width":7},
{"from":"Solid", "to":"Heating and cooling - homes", "width":3},
{"from":"Liquid", "to":"Heating and cooling - homes", "width":3},
{"from":"Gas", "to":"Heating and cooling - homes", "width":81},
{"from":"Electricity grid", "to":"Heating and cooling - commercial", "width":7},
{"from":"Solid", "to":"Heating and cooling - commercial", "width":1},
{"from":"Liquid", "to":"Heating and cooling - commercial", "width":2},
{"from":"Gas", "to":"Heating and cooling - commercial", "width":19},
{"from":"Electricity grid", "to":"Lighting & appliances - homes", "width":21},
{"from":"Gas", "to":"Lighting & appliances - homes", "width":2},
{"from":"Electricity grid", "to":"Lighting & appliances - commercial", "width":18},
{"from":"Gas", "to":"Lighting & appliances - commercial", "width":2},
{"from":"Electricity grid", "to":"Industry", "width":30},
{"from":"Solid", "to":"Industry", "width":13},
{"from":"Liquid", "to":"Industry", "width":34},
{"from":"Gas", "to":"Industry", "width":54},
{"from":"Electricity grid", "to":"Agriculture", "width":1},
{"from":"Solid", "to":"Agriculture", "width":1},
{"from":"Liquid", "to":"Agriculture", "width":1},
{"from":"Gas", "to":"Agriculture", "width":1},
{"from":"Electricity grid", "to":"Road transport", "width":1},
{"from":"Liquid", "to":"Road transport", "width":122},
{"from":"Electricity grid", "to":"Rail transport", "width":2},
{"from":"Liquid", "to":"Rail transport", "width":1},
{"from":"Liquid", "to":"Domestic aviation", "width":2},
{"from":"Liquid", "to":"National navigation", "width":4},
{"from":"Liquid", "to":"International aviation", "width":38},
{"from":"Liquid", "to":"International shipping", "width":13},
{"from":"Electricity grid", "to":"Geosequestration", "width":1},
{"from":"Gas", "to":"Losses", "width":2}
 ]}
    </textarea>
  </div>
  This sample demonstrates one way of generating a Sankey or flow diagram.
  The data was derived from <a href="https://tamc.github.io/Sankey/">https://tamc.github.io/Sankey/</a>.
</div>
</body>
</html>

Walter, you are a rock star! Your solution worked. Thank you for your help.