Search code examples
amchartssankey-diagram

Amcharts sankey diagram: how to see ALL 'following' path on hovering a link


I'm very interested in doing a "Traceable Sankey Diagram" using AmCharts exactly like the one that can be experienced here: https://www.amcharts.com/demos/traceable-sankey-diagram/

When you move the mouse over a link (for instance C-F) you can see other links highlighted (i.e. F-I + I-Q). enter image description here That's a GREAT feature but I'd like it to be slightly different. I would like that all "compatible" links to be highlighted (F-I but also F-M. Then I-Q, I-R, I-S, I-T and M-U) because all those can be following C-F.

Is it feasible ? I couldn't find any option in Amchart's doc for that. Thanks,


Solution

  • What is required is a new algorithm to select items that are to be highlighted on link hover.

    To select all compatible targets and their targets, ... and so on, one may:

    1. start the array itemsToHighlight with the item under the pointer, and store in an array allTargetIds initially the target id of that item
    2. for all links of the series add to the itemsToHighlight all those items whose source id is equal to one of the target ids in the allTargetIds array.
    3. repeat step 2. until there is no new item added to itemsToHighlight.

    Now, if one also wants to also highlight compatible items to the left, that is all possible sources and all their possible sources, ... and so on, the algorithm is the same, but invert source and target positions.

    Here's that part in code:

    var itemsToHighlight = [];
    series.links.template.events.on("pointerover", function(event) {
        var dataItem = event.target.dataItem;
        itemsToHighlight = [dataItem];
        var allTargetIds = [dataItem.get("targetId")];
        var allSourceIds = [dataItem.get("sourceId")];
    
        // all compatible (possible) targets, and their possible
        // targets, and so on until nothing new is found 
        var newItems = true;
        while (newItems) {
            newItems = false;
            am5.array.each(series.dataItems, function(dataItem) {
                if (itemsToHighlight.indexOf(dataItem) < 0 &&
                        allTargetIds.indexOf(dataItem.get("sourceId")) >= 0) {
                    allTargetIds.push(dataItem.get("targetId"));
                    itemsToHighlight.push(dataItem);
                    newItems = true;
                }
            });
        }
    
        // all compatible (possible) sources, and their possible
        // sources, and so on until nothing new is found 
        newItems = true;
        while (newItems) {
            newItems = false;
            am5.array.each(series.dataItems, function(dataItem) {
                if (itemsToHighlight.indexOf(dataItem) < 0 &&
                        allSourceIds.indexOf(dataItem.get("targetId")) >= 0) {
                    allSourceIds.push(dataItem.get("sourceId"));
                    itemsToHighlight.push(dataItem);
                    newItems = true;
               }
            });
        }
      
        am5.array.each(itemsToHighlight, function(dataItem) {
            dataItem.get("link").hover();
        });
    });
    
    series.links.template.events.on("pointerout", function(event) {    
        am5.array.each(itemsToHighlight, function(dataItem) {
            dataItem.get("link").unhover();
        });
        itemsToHighlight = [];
    });
    

    This is implemented for the links only, as in the example we started from. Nodes keep their default behaviour to highlight only their direct sources and targets. However, a behaviour similar to links can be extended to the nodes too, with minimal adaptations.

    The adapted example in a snippet:

    /**
     * ---------------------------------------
     * This demo was created using amCharts 5.
     * 
     * For more information visit:
     * https://www.amcharts.com/
     * 
     * Documentation is available at:
     * https://www.amcharts.com/docs/v5/
     * ---------------------------------------
     */
    
    // Create root element
    // https://www.amcharts.com/docs/v5/getting-started/#Root_element
    var root = am5.Root.new("chartdiv");
    
    // Set themes
    // https://www.amcharts.com/docs/v5/concepts/themes/
    root.setThemes([am5themes_Animated.new(root)]);
    
    // Create series
    // https://www.amcharts.com/docs/v5/charts/flow-charts/
    var series = root.container.children.push(
      am5flow.Sankey.new(root, {
        sourceIdField: "from",
        targetIdField: "to",
        valueField: "value",
        paddingRight: 50,
        idField: "id"
      })
    );
    
    series.links.template.setAll({
      fillStyle: "solid",
      fillOpacity: 0.15
    });
    
    
    var itemsToHighlight = [];
    series.links.template.events.on("pointerover", function(event) {
      var dataItem = event.target.dataItem;
      itemsToHighlight = [dataItem];
      var allTargetIds = [dataItem.get("targetId")];
      var allSourceIds = [dataItem.get("sourceId")];
    
      // all compatible (possible) targets, and their possible 
      // targets, and so on until nothing new is found 
      var newItems = true;
      while (newItems) {
        newItems = false;
        am5.array.each(series.dataItems, function(dataItem) {
          if (itemsToHighlight.indexOf(dataItem) < 0 &&
            allTargetIds.indexOf(dataItem.get("sourceId")) >= 0) {
            allTargetIds.push(dataItem.get("targetId"));
            itemsToHighlight.push(dataItem);
            newItems = true;
          }
        });
      }
    
      // all compatible (possible) sources, and their possible 
      // sources, and so on until nothing new is found 
      newItems = true;
      while (newItems) {
        newItems = false;
        am5.array.each(series.dataItems, function(dataItem) {
          if (itemsToHighlight.indexOf(dataItem) < 0 &&
            allSourceIds.indexOf(dataItem.get("targetId")) >= 0) {
            allSourceIds.push(dataItem.get("sourceId"));
            itemsToHighlight.push(dataItem);
            newItems = true;
          }
        });
      }
      
      am5.array.each(itemsToHighlight, function(dataItem) {
        dataItem.get("link").hover();
      });
    });
    
    series.links.template.events.on("pointerout", function(event) {
      am5.array.each(itemsToHighlight, function(dataItem) {
        dataItem.get("link").unhover();
      });
      itemsToHighlight = [];
    });
    
    // Set data
    // https://www.amcharts.com/docs/v5/charts/flow-charts/#Setting_data
    series.data.setAll([{
        from: "A",
        to: "E",
        value: 1,
        id: "A0-0"
      },
      {
        from: "A",
        to: "F",
        value: 1,
        id: "A1-0"
      },
      {
        from: "A",
        to: "G",
        value: 1,
        id: "A2-0"
      },
    
      {
        from: "B",
        to: "E",
        value: 1,
        id: "B0-0"
      },
      {
        from: "B",
        to: "F",
        value: 1,
        id: "B1-0"
      },
      {
        from: "B",
        to: "G",
        value: 1,
        id: "B2-0"
      },
    
      {
        from: "C",
        to: "F",
        value: 1,
        id: "C0-0"
      },
      {
        from: "C",
        to: "G",
        value: 1,
        id: "C1-0"
      },
      {
        from: "C",
        to: "H",
        value: 1,
        id: "C2-0"
      },
    
      {
        from: "D",
        to: "E",
        value: 1,
        id: "D0-0"
      },
      {
        from: "D",
        to: "F",
        value: 1,
        id: "D1-0"
      },
      {
        from: "D",
        to: "G",
        value: 1,
        id: "D2-0"
      },
      {
        from: "D",
        to: "H",
        value: 1,
        id: "D3-0"
      },
    
      {
        from: "E",
        to: "I",
        value: 1,
        id: "A0-1"
      },
      {
        from: "E",
        to: "I",
        value: 1,
        id: "B0-1"
      },
      {
        from: "E",
        to: "L",
        value: 1,
        id: "D0-1"
      },
    
      {
        from: "F",
        to: "I",
        value: 1,
        id: "A1-1"
      },
      {
        from: "F",
        to: "I",
        value: 1,
        id: "C0-1"
      },
      {
        from: "F",
        to: "I",
        value: 1,
        id: "D1-1"
      },
      {
        from: "F",
        to: "M",
        value: 1,
        id: "B1-1"
      },
    
      {
        from: "G",
        to: "I",
        value: 1,
        id: "A2-1"
      },
      {
        from: "G",
        to: "I",
        value: 1,
        id: "B2-1"
      },
      {
        from: "G",
        to: "J",
        value: 1,
        id: "C1-1"
      },
      {
        from: "G",
        to: "N",
        value: 1,
        id: "D2-1"
      },
    
      {
        from: "H",
        to: "K",
        value: 1,
        id: "C2-1"
      },
      {
        from: "H",
        to: "N",
        value: 1,
        id: "D3-1"
      },
    
      {
        from: "I",
        to: "O",
        value: 1,
        id: "A0-2"
      },
      {
        from: "I",
        to: "O",
        value: 1,
        id: "B2-2"
      },
      {
        from: "I",
        to: "Q",
        value: 1,
        id: "A1-2"
      },
      {
        from: "I",
        to: "R",
        value: 1,
        id: "A2-2"
      },
      {
        from: "I",
        to: "S",
        value: 1,
        id: "D1-2"
      },
      {
        from: "I",
        to: "T",
        value: 1,
        id: "B0-2"
      },
      {
        from: "I",
        to: "Q",
        value: 1,
        id: "C0-2"
      },
    
      {
        from: "J",
        to: "U",
        value: 1,
        id: "C1-2"
      },
    
      {
        from: "K",
        to: "V",
        value: 1,
        id: "C2-2"
      },
      {
        from: "M",
        to: "U",
        value: 1,
        id: "B1-2"
      },
    
      {
        from: "N",
        to: "Q",
        value: 1,
        id: "D2-2"
      },
      {
        from: "N",
        to: "Q",
        value: 1,
        id: "D3-2"
      },
    
      {
        from: "L",
        to: "W",
        value: 1,
        id: "D0-2"
      }
    ]);
    
    // Make stuff animate on load
    series.appear(1000, 100);
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
    }
    
    #chartdiv {
      width: 100%;
      height: 500px;
    }
    <script src="https://cdn.amcharts.com/lib/5/index.js"></script>
    <script src="https://cdn.amcharts.com/lib/5/flow.js"></script>
    <script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
    <div id="chartdiv"></div>

    or in a jsFiddle.