suswang8
suswang8

Reputation: 31

Highcharts Scatter Plot - How to Fix Overlapping Data Labels?

I have seen many posts on this topic, but it doesn't seem the issue has ever been properly addressed.

We have a large scatter with about 30 points on it (nothing overwhelming). But in certain cases, the dots will be very close together or overlapping (not much we can really do about that, I guess).

The main problem is that we want the data labels visible at all times, and these data labels are overlapping when the points are close to each other.

We have tried allowOverlap: false, but that's not really what we need/want. Our ideal outcome is allowing all datalabels to be displayed on screen inside the scatter while still being able to read each one at all times.

Do we fix this by adjusting the separation of the dots or by adjusting the separation/padding of the datalabels? Any suggestions? Thank you.

Upvotes: 2

Views: 1687

Answers (2)

Sebastian Bochan
Sebastian Bochan

Reputation: 37578

You can try to adapt this algorithm:

    function StaggerDataLabels(series) {
    sc = series.length;
    if (sc < 2) return;

    for (s = 1; s < sc; s++) {
        var s1 = series[s - 1].points,
            s2 = series[s].points,
            l = s1.length,
            diff, h;

        for (i = 0; i < l; i++) {
            if (s1[i].dataLabel && s2[i].dataLabel) {
                diff = s1[i].dataLabel.y - s2[i].dataLabel.y;
                h = s1[i].dataLabel.height + 2;

                if (isLabelOnLabel(s1[i].dataLabel, s2[i].dataLabel)) {
                    if (diff < 0) s1[i].dataLabel.translate(s1[i].dataLabel.translateX, s1[i].dataLabel.translateY - (h + diff));
                    else s2[i].dataLabel.translate(s2[i].dataLabel.translateX, s2[i].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;
}

http://jsfiddle.net/menXU/6/

Upvotes: 0

Ivaylo Marinovski
Ivaylo Marinovski

Reputation: 143

I haven't found a working configuration solution of this problem from Highcharts (although I cannot guarantee there isn't one in latest version). However there are some algorithms for acceptable randomization of the labels coordinates that split data labels.

Here are some useful links that could help you with the algorithm:

wordcloud package in R (cloud.R is the file containing the algorithm)

direct labels package in R

And some dummy pseudo code translation in JavaScript of the R code would be:

splitLabels: function() {
    // Create an array of x-es and y-es that indicate where your data lie
    var xArr = getAllDataX();
    var yArr = getAllDataY();

    var labelsInfo = {};
    this.chartSeries.forEach(function(el) {
        var text = el.data.name;
        labelsInfo[el.data.id] = {
            height: getHeight(text),
            width: getWidth(text),
            text: text
        };
    }, this);

    var sdx = getStandardDeviation(xArr);
    var sdy = getStandardDeviation(yArr);
    if(sdx === 0) sdx = 1;
    if(sdy === 0) sdy = 1;

    var boxes = [];

    var xlim = [], ylim = [];
    xlim[0] = this.chart.xAxis[0].getExtremes().min;
    xlim[1] = this.chart.xAxis[0].getExtremes().max;
    ylim[0] = this.chart.yAxis[0].getExtremes().min;
    ylim[1] = this.chart.yAxis[0].getExtremes().max;

    for (var i = 0; i < data.length; i++) {
        var pointX = data[i].x;
        var pointY = data[i].y;
        if (pointX<xlim[0] || pointY<ylim[0] || pointX>xlim[1] || pointY>ylim[1]) continue;
        var theta = Math.random() * 2 * Math.PI,
            x1 = data[i].x,
            x0 = data[i].x,
            y1 = data[i].y,
            y0 = data[i].y,
            width = labelsInfo[data[i].id].width,
            height = labelsInfo[data[i].id].height ,
            tstep = Math.abs(xlim[1] - xlim[0]) > Math.abs(ylim[1] - ylim[0]) ? Math.abs(ylim[1] - ylim[0]) / 100 : Math.abs(xlim[1] - xlim[0]) / 100,
            rstep = Math.abs(xlim[1] - xlim[0]) > Math.abs(ylim[1] - ylim[0]) ? Math.abs(ylim[1] - ylim[0]) / 100 : Math.abs(xlim[1] - xlim[0]) / 100,
            r = 0;

        var isOverlapped = true;
        while(isOverlapped) {
            if((!hasOverlapped(x1-0.5*width, y1-0.5*height, width, height, boxes)
                && x1-0.5*width>xlim[0] && y1-0.5*height>ylim[0] && x1+0.5*width<xlim[1] && y1+0.5*height<ylim[1]) )
            {
                boxes.push({
                    leftX: x1-0.5*width,
                    bottomY: y1-0.5*height,
                    width: width,
                    height: height,
                    icon: false,
                    id: data[i].id,
                    name: labelsInfo[data[i].id].text
                });
                data[i].update({
                    name: labelsInfo[data[i].id].text,
                    dataLabels: {
                        x: (x1 - data[i].x),
                        y: (data[i].y - y1)
                    }
                }, false);
                isOverlapped = false;
            } else {
                theta = theta+tstep;
                r = r + rstep*tstep/(2*Math.PI);
                x1 = x0+sdx*r*Math.cos(theta);
                y1 = y0+sdy*r*Math.sin(theta);
            }
        }
    }
    // You may have to redraw the chart here
},

You can call this function on redraw or optimized to call it less often.

Please note that if you have some big points or shapes or icons indicating where your data items lie you will have to check if any of the proposed solutions does not interfere(overlap) with the icons as well.

Upvotes: 2

Related Questions