'How to draw line from Data label to Marker in Highchart Scatter Plot

I'm using HighCharts in a .Net Core application. I have a scatter plot, that I'm using jitter to separate the points. I need to show the data labels pointing to the point markers, but they overlap. Here is my code:

@using Highsoft.Web.Mvc.Charts;
@using Highsoft.Web.Mvc.Charts.Rendering;
<script src="~/lib/highcharts/js/v10/highcharts.js"></script>
<script src="~/lib/highcharts/js/v10/highcharts-more.js"></script>
<script src="~/lib/highcharts/js/v10/annotations.js"></script>

<link rel="stylesheet" type="text/css" href="~/lib/highcharts/highcharts.css" />
<script type="text/javascript">
    $(function () {
             $('#container1').highcharts({
            chart: {
                 type: 'scatter',
                 zoomType: 'xy',
                 width: 1500,
                 height: 1000,
                 events: {
                     load: function () {
                         StaggerDataLabels(this.series);
                     },
                     redraw: function () {
                         var series = this.series;
                         setTimeout(function () {
                             StaggerDataLabels(series);
                         }, 1000);
                     }
                 },
            },
            credits: {
                enabled: false
            },
            legend: {
                enabled: false
            },
            title: {
                text: 'Impact vs Life Cycle Root Cause',
                style: {
                    fontSize: '24px',
                    color: 'black'
                },
            },
            xAxis: {
                gridLineWidth: 1,
                title: {
                    text: 'Impact',
                    style: {
                        fontSize: '20px',
                        color: 'black'
                    },
                },
                tickInterval: 1,
                min: 0,
                max: 6,
                showFirstLabel: false,
                showLastLabel: false,
                labels: {
                    formatter: function () {
                        return bcxAxislabelFormatter(this.value);
                    },
                }
            },
            yAxis: {
                title: {
                    text: 'Life Cycle Root Cause',
                    style: {
                        fontSize: '20px',
                        color: 'black'
                    },
                },
                tickInterval: 1,
                min: 0,
                max: 6,
                showFirstLabel: false,
                showLastLabel: false,
                labels: {
                    formatter: function () {
                        return bcyAxislabelFormatter(this.value);
                    },
                    rotation: -45,
                }
            },
            tooltip: {
                formatter: formatB1ToolTip,
            },
            plotOptions: {
               series: {
                    dataLabels: {
                       allowOverlap: true,
                       shape: 'connector',
                       enabled: true,
                       formatter: function () {
                        return (this.point.label);
                    }

            }
        },
                scatter: {
                    jitter: {
                        x: 0.24,
                        y: 0.24
                    },
            marker: {
                radius: 5,
                states: {
                    hover: {
                        enabled: true,
                        lineColor: 'rgb(100,100,100)'
                    }
                }
            },
            states: {
                hover: {
                    marker: {
                        enabled: false
                    }
                }
            },
            tooltip: {
                pointFormat: '{point.x} , {point.y} '
            }
        }
            },
            series: [{
                data:@Html.Raw(Newtonsoft.Json.JsonConvert.DeserializeObject(ViewData["scatter1"].ToString()))
            }]
        });
    });
        function handleClick(e) {

        var url = '@Url.Action("GetIssueData", "Chart")' + "?X=" + e.point.x + "&Y=" + e.point.y;
        $.get(url).done(function (data) {
            $('#DisplayDetailedContent').find('#modal-content2').html(data);
        });
        $(this).attr('data-target', '#modal-container2');
        $(this).attr('data-toggle', 'modal');
        $('#DisplayDetailedContent').find('#modal-container2').modal('show');

    }
    function formatB1ToolTip() {
        var ptx = bcxAxislabelFormatter(this.point.x);
        var pty = bcyAxislabelFormatter(this.point.y);
        return '<b>' + ptx + '/' + pty + '</b><br/>Total Issues: ' + this.point.z ;
    }

    function bcxAxislabelFormatter(x) {
        var label = "";
        if (x == '1') {
            label = "Minimal";
        } else if (x == '2') {
            label = "Minor";
        } else if (x == '3') {
            label = "Moderate";
        } else if (x == '4') {
            label = "Significant";
        } else {
            label = "Critical";
        }
        return label;
    }

    function bcyAxislabelFormatter(x) {
        var label = "";
        if (x == '1') {
            label = "Test & Ops";
        } else if (x == '2') {
            label = "Manufacturing";
        } else if (x == '3') {
            label = "Process & Review";
        } else if (x == '4') {
            label = "Design & Process";
        } else {
            label = "Requirements";
        }
        return label;
    }

    function StaggerDataLabels(series) {
        sc = series[0].points.length;
        for (x = 1; x < 6; x++) {
            for (y = 1; y < 6; y++) {
                for (z = 1; z < 6; z++) {

                var arr = [];
                for (s = 1; s < sc; s++) {
                    if (series[0].points[s - 1].dataLabels[0].element.point.x == x && series[0].points[s - 1].dataLabels[0].element.point.y == y) {
                        arr.push(series[0].points[s - 1]);
                    }
                }
                var ac = arr.length;
                if (ac > 1) {
                    for (s = 1; s < ac; s++) {
                        var s1 = arr[s - 1],
                            s2 = arr[s],
                            diffx,
                            diff, h;
                        if (s1.dataLabel && s2.dataLabel) {
                            diff = s1.dataLabel.y - s2.dataLabel.y;
                            h = s1.dataLabel.height + 1;
                            if (isLabelOnLabel(s1.dataLabel, s2.dataLabel)) {
                                if (diff < 0) s1.dataLabel.translate(s1.dataLabel.translateX, s1.dataLabel.translateY - (h + diff));
                                else s2.dataLabel.translate(s2.dataLabel.translateX, s2.dataLabel.translateY - (h - diff));
                               
                            }
                        }
                    }
                }
            }
            }
        }
    }

    //compares two datalabels and returns true if they overlap


    function isLabelOnLabel(a, b) {
        var al = a.x - (a.width / 2);
        var ar = a.x + (a.width / 2);
        var bl = b.x - (b.width / 2);
        var br = b.x + (b.width / 2);

        var at = a.y;
        var ab = a.y + a.height;
        var bt = b.y;
        var bb = b.y + b.height;

        if (bl > ar || br < al) {
            return false;
        } //overlap not possible
        if (bt > ab || bb < at) {
            return false;
        } //overlap not possible
        if (bl > al && bl < ar) {
            return true;
        }
        if (br > al && br < ar) {
            return true;
        }

        if (bt > at && bt < ab) {
            return true;
        }
        if (bb > at && bb < ab) {
            return true;
        }

        return false;
    }
</script>


<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
    <style type="text/css">
        .table1 .highcharts-background {
            stroke: black;
            stroke-width: 2px;
        }

        .table1 .highcharts-plot-border {
            stroke-width: 1px;
            stroke: gray;
        }

        .table1 .highcharts-plot-background {
            fill: url(#MyGradient)
        }

        .table1 .highcharts-axis-title {
            font-size: 14px;
        }
    </style>
    <defs>
        <linearGradient id="MyGradient" x1="0%" y1="100%" x2="100%" y2="0%">
            <stop offset="0%" stop-color="#02f512" />
            <stop offset="30%" stop-color="#F2E95D" />
            <stop offset="70%" stop-color="#F2E95D" />
            <stop offset="100%" stop-color="#f50202" />
        </linearGradient>
    </defs>
</svg>


@{
    ViewData["Title"] = "Privacy Policy";
}

<div id="DisplayDataContent">
    <div id="DisplayDetailedContent">
        <div id="modal-container2" class="modal fade" tabindex="-1">
            <div class="modal-dialog modal-lg">
                <div id="modal-content2" class="modal-content">
                </div>
            </div>
        </div>
    </div>

</div>
<div>
    <table class="table1" style="margin:auto">
        <tr>
            <td>
                Container
                <!--Bubble Chart-->
                <div id="container"></div>
            </td>
        </tr>
        <tr>
            <td>
                container 1
                <!--Bubble Chart-->
                <div id="container1"></div>
            </td>
        </tr>
    </table>
</div>

@section Scripts {


}
<script>
    $(function () {
        $('body').on('click', '.close_issues', function (e) {
            e.preventDefault();
            $('#DisplayDetailedContent').find('#modal-container2').modal('hide');
        })
    });
</script>

Here is my dataset:

[
{"x":1,"y":2,"label":"Second Issue Title","id":null},
{"x":1,"y":2,"label":"Test Not Admin","id":null},
{"x":1,"y":2,"label":"Commit Quarter Test","id":null},
{"x":1,"y":2,"label":"this is a test 1","id":null},
{"x":1,"y":2,"label":"Security Marking Test 2","id":null},
{"x":1,"y":3,"label":"Create Commit Quarter History 3","id":null},
{"x":1,"y":3,"label":"Test of TinyMCE","id":null},
{"x":1,"y":4,"label":"TPPI Test","id":null},
{"x":2,"y":2,"label":"Test LMPI #2","id":null},
{"x":2,"y":2,"label":"Test TinyMCE Changes","id":null},
{"x":2,"y":2,"label":"Test Affected programs #1","id":null},
{"x":2,"y":2,"label":"Test Edge","id":null},
{"x":2,"y":3,"label":"Testing IssueID Loading","id":null},
{"x":2,"y":3,"label":"Third Test of Issue ID Creation","id":null},
{"x":2,"y":3,"label":"This is the title of the 1st issue which is a little longer than normal","id":null},
{"x":2,"y":3,"label":"This is a test","id":null},
{"x":2,"y":3,"label":"DELETED","id":null},
{"x":2,"y":3,"label":"Test","id":null},
{"x":2,"y":4,"label":"Yet another issue yup","id":null},
{"x":2,"y":4,"label":"Testing of Create an Issue page","id":null},
{"x":3,"y":1,"label":"Second test of IssueID Creation","id":null},
{"x":3,"y":2,"label":"New Issue on 10/19","id":null},
{"x":3,"y":3,"label":"this is a test ","id":null},
{"x":3,"y":3,"label":"Testing New IssueID Creation","id":null},
{"x":3,"y":3,"label":"Testing Root Cause Issues","id":null},
{"x":3,"y":4,"label":"DELETED","id":null},
{"x":3,"y":4,"label":"DELETED","id":null},
{"x":3,"y":4,"label":"test","id":null},
{"x":3,"y":5,"label":"DELETED","id":null},
{"x":3,"y":5,"label":"Security Marking Test 3","id":null},
{"x":4,"y":1,"label":"Testing of Issue ID4","id":null},
{"x":4,"y":2,"label":"Create Commit Quarter History 2","id":null},
{"x":4,"y":2,"label":"Lifecycle and Impact test","id":null},
{"x":4,"y":3,"label":"This is a test","id":null},
{"x":4,"y":3,"label":"New Functional Area Test","id":null},
{"x":4,"y":3,"label":"this is a test 1","id":null},
{"x":4,"y":3,"label":"DELETED","id":null},
{"x":4,"y":3,"label":"Test4","id":null},
{"x":4,"y":5,"label":"Name Test","id":null},
{"x":4,"y":5,"label":"Test of LMPI","id":null},
{"x":5,"y":2,"label":"test1","id":null},
{"x":5,"y":3,"label":"Edge Test 2","id":null},
{"x":5,"y":3,"label":"TPPI Test","id":null},
{"x":5,"y":3,"label":"Security Marking Test","id":null},
{"x":5,"y":5,"label":"Test2","id":null}
]

I have seen the following post: how-to-render-a-line-from-datalabels-to-marker-on-graph-in-highcharts but I can't figure out how to utilize/call the following function:

Highcharts.SVGRenderer.prototype.symbols.connector = function(x, y, w, h, options) {
  var anchorX = options && options.anchorX,
    anchorY = options && options.anchorY,
    path,
    yOffset,
    lateral = w / 2,
    H = Highcharts;

  if (H.isNumber(anchorX) && H.isNumber(anchorY)) {

    path = ['M', anchorX, anchorY];

    // Prefer 45 deg connectors
    yOffset = y - anchorY;
    if (yOffset < 0) {
      yOffset = -h - yOffset;
    }
    if (yOffset < w) {
      lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset;
    }

    // Anchor below label
    if (anchorY > y + h) {
      path.push('L', x + lateral, y + h);

      // Anchor above label
    } else if (anchorY < y) {
      path.push('L', x + lateral, y);

      // Anchor left of label
    } else if (anchorX < x) {
      path.push('L', x, y + h / 2);

      // Anchor right of label
    } else if (anchorX > x + w) {
      path.push('L', x + w, y + h / 2);
    }
  }
  return path || [];
};

How can I draw a line from the label to the point?



Solution 1:[1]

You can add another scatter series with and modify it a bit to show connections and dataLabels.

series: [{
      type: 'scatter',
      data: [{
        name: 'First',
        x: 0,
        y: 29.9,
        dataLabels: {
          enabled: false
        }
      }, {
        name: 'Second',
        x: 0.1,
        y: 39.9,
        dataLabels: {
          formatter: function() {
            return 'test label';
          },
          x: 20,
          y: 0
        },
        marker: {
          enabled: false,
          radius: 0
        }
      }],
    },
]

Options chart to set behavior, with enableMouseTracking: false to turn off mouse tracking, lineWidth: 1 to draw line (connections to point) and showInLegend: false for hide series in legend.

  plotOptions: {
    scatter: {
      states: {
        inactive: {
          enabled: false
        }
      },
      dataLabels: {
        enabled: true,
        allowOverlap: true,
        crop: false,
        overflow: 'allow',
        align: 'center',
        verticalAlign: 'bottom',
        useHTML: true,
      },
      lineWidth: 1,
      color: 'red',
      enableMouseTracking: false,
      showInLegend: false
    },
  }

live demo: http://jsfiddle.net/BlackLabel/hpds8f0x/2/

Solution 2:[2]

The missing return statement is the initial problem, but more broadly speaking, this is not a true inplace algorithm, because it still reserves O(n) auxiliary memory by creating new arrays.

For an algorithm to be inplace, there should be no O(n) auxiliary memory usage. Instead make also the splitArray and mergeArray inplace functions, so that at all times there is only one array that is being mutated.

For that to happen, you would need to pass the start/end indices of the subarray that is going to be subject of the split/merge operation.

It is also safer to include the empty-array case in the first base case of splitArray: so use <= 1 instead of === 1.

Here is your code altered with that idea:

let reverseArrayInPlace = (arr, start=0, end=ar.length) => {

    let splitArray = (start, end) => {
        if (end - start <= 1) return;
        if (end - start === 2) {
            let temp = arr[start]; arr[start] = arr[start+1]; arr[start+1] = temp;
        }
        else{
            reverseArrayInPlace(arr, start, end);
        }
    }
 
    let mergeArray = (start, mid, end) => {
        arr.splice(start, 0, ...arr.splice(mid, end - mid));
    }

    let half = (start + end) >> 1;
    splitArray(start, half);
    splitArray(half, end);
    mergeArray(start, half, end);
    return arr;
}

let ar = [1, 5, 0, 4, 6];
console.log(reverseArrayInPlace(ar));

You also don't really need to deal with the second base case separately. The operation will work fine if you deal with that case as a recursive case.

Here is your code altered with that idea:

let reverseArrayInPlace = (arr, start=0, end=ar.length) => {

    let splitArray = (start, end) => {
        if (end - start > 1) reverseArrayInPlace(arr, start, end);
    }
  
    let mergeArray = (start, mid, end) => {
        arr.splice(start, 0, ...arr.splice(mid, end - mid));
    }
  
    let half = (start + end) >> 1;
    splitArray(start, half);
    splitArray(half, end);
    mergeArray(start, half, end);
    return arr;
}

let ar = [1, 5, 0, 4, 6];
console.log(reverseArrayInPlace(ar));

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Sebastian Hajdus
Solution 2