Search code examples
highchartssankey-diagram

Highcharts sankey node without links


I have a highcharts sankey diagram with two sides:

enter image description here

There are situations where some of my nodes have empty links (=with 0 weight). I would like the node to being displayed despite having no link from or to it.

Any chance I can achieve this?

I read on this thread that I have to fake it with weight=1 connexions, I could make the link transparent, and twitch the tooltip to hide those, but that's very painful for something that feels pretty basic.

Maybe a custom call of the generateNode call or something?

Thanks for the help


Solution

  • You can use the following wrap to show a node when the weight is 0.

     const isObject = Highcharts.isObject,
       merge = Highcharts.merge
    
     function getDLOptions(
       params
     ) {
       const optionsPoint = (
           isObject(params.optionsPoint) ?
           params.optionsPoint.dataLabels : {}
         ),
         optionsLevel = (
           isObject(params.level) ?
           params.level.dataLabels : {}
         ),
         options = merge({
           style: {}
         }, optionsLevel, optionsPoint);
       return options;
     }
    
     Highcharts.wrap(
       Highcharts.seriesTypes.sankey.prototype,
       'translateNode',
       function(proceed, node, column) {
         var translationFactor = this.translationFactor,
           series = this,
           chart = this.chart,
           options = this.options,
           sum = node.getSum(),
           nodeHeight = Math.max(Math.round(sum * translationFactor),
             this.options.minLinkWidth),
           nodeWidth = Math.round(this.nodeWidth),
           crisp = Math.round(options.borderWidth) % 2 / 2,
           nodeOffset = column.sankeyColumn.offset(node,
             translationFactor),
           fromNodeTop = Math.floor(Highcharts.pick(nodeOffset.absoluteTop, (column.sankeyColumn.top(translationFactor) +
             nodeOffset.relativeTop))) + crisp,
           left = Math.floor(this.colDistance * node.column +
             options.borderWidth / 2) + Highcharts.relativeLength(node.options.offsetHorizontal || 0,
             nodeWidth) +
           crisp,
           nodeLeft = chart.inverted ?
           chart.plotSizeX - left :
           left;
         node.sum = sum;
    
         proceed.apply(this, Array.prototype.slice.call(arguments, 1));
    
         if (1) {
           // Draw the node
           node.shapeType = 'rect';
    
           node.nodeX = nodeLeft;
           node.nodeY = fromNodeTop;
    
           let x = nodeLeft,
             y = fromNodeTop,
             width = node.options.width || options.width || nodeWidth,
             height = node.options.height || options.height || nodeHeight;
    
           if (chart.inverted) {
             x = nodeLeft - nodeWidth;
             y = chart.plotSizeY - fromNodeTop - nodeHeight;
             width = node.options.height || options.height || nodeWidth;
             height = node.options.width || options.width || nodeHeight;
           }
    
           // Calculate data label options for the point
           node.dlOptions = getDLOptions({
             level: (this.mapOptionsToLevel)[node.level],
             optionsPoint: node.options
           });
    
           // Pass test in drawPoints
           node.plotX = 1;
           node.plotY = 1;
    
           // Set the anchor position for tooltips
           node.tooltipPos = chart.inverted ? [
             (chart.plotSizeY) - y - height / 2,
             (chart.plotSizeX) - x - width / 2
           ] : [
             x + width / 2,
             y + height / 2
           ];
    
           node.shapeArgs = {
             x,
             y,
             width,
             height,
             display: node.hasShape() ? '' : 'none'
           };
         } else {
           node.dlOptions = {
             enabled: false
           };
         }
       }
     );
    

    Demo: http://jsfiddle.net/BlackLabel/uh6fp89j/

    In the above solution, another node arrangement would be difficult to achieve and may require a lot of modifications beyond our scope of support.

    You can consider using mentioned "tricky solution", since might return a better positioning result. This solution is based on changing 0 weight nodes on the chart.load() event and converting the tooltip as well, so it may require adjustment to your project.

       chart: {
         events: {
           load() {
             this.series[0].points.forEach(point => {
               if (point.weight === 0) {
                 point.update({
                   weight: 0.1,
                   color: 'transparent'
                 })
               }
             })
           }
         }
       },
    
       tooltip: {
         nodeFormatter: function() {
           return `${this.name}: <b>${Math.floor(this.sum)}</b><br/>`
         },
         pointFormatter: function() {
           return `${this.fromNode.name} → ${this.toNode.name}: <b>${Math.floor(this.weight)}</b><br/>`
         }
       },
    

    Demo: http://jsfiddle.net/BlackLabel/0dqpabku/