Highcharts Hijinks

We at InsightSquared are big fans of the Open Source community, and one project that we’ve found very useful for rendering our complex analytics in a clean, straightforward manner is the Highcharts Javascript library (http://www.highcharts.com/).

Below we’ve outlined one of the ways we’ve pushed Highcharts to create state-of-the-art data visualizations. This is not for the faint of code, so reader beware!

 

Injecting features into an external library is like sticking a needle back into the center of a haystack. In this particular case, the needle was an arrow, and the haystack was a Highcharts line graph:

Highcharts, the world’s #1 Javascript charting library, supports many different marker symbols, including circles, diamonds, squares, and even custom images… but no arrows.

Why not just use the image of an arrow? That idea seemed promising, but it came with serious limitations. Customizing the arrow’s rotation, color, and size would require a lot of extra work – one would need to inject generated images of arrows into the chart options, in the process adding precious milliseconds onto the load time. Furthermore, if the chart were resized, the arrow would no longer be pointing in the right direction:

Pasted Graphic 3.tiff

I felt there had to be a cleaner approach. A quick Google search for “highcharts arrow module” revealed that this had already been tackled – I found this tweet by the Highcharts team that demonstrated the functionality I needed. No image generator necessary! Unfortunately, it suffered from the same resizing issue presented as before, only in an even less desirable way:

What went wrong in this  “official” hack?

The sole purpose of their code is to over-ride the Highcharts function referenced by Highcharts.seriesTypes.spline.prototype.drawGraph:

    var SplineSeries = Highcharts.seriesTypes.spline;

        // override the drawLine method

        var splineDrawGraph = SplineSeries.prototype.drawGraph;

        SplineSeries.prototype.drawGraph = function() {

        …

        …

    };

Even though drawGraph is an “internal” function, Highcharts exposes its internals to support modification. Therefore, we can override a method like drawGraph by assigning its reference to a new function, as demonstrated above.

What happens inside the override? First, the original drawGraph method gets called to (presumably) do the normal rendering:

    // call the original method

    splineDrawGraph.apply(series, arguments);

Afterwards, the angle and positioning of the arrow are determined by examining the series and point data:

    var arrowLength = 15,

        arrowWidth = 8,

        series = this,

        segments = series.splinedata || series.segments,

        lastSeg = segments[segments.length 1],

        lastPoint = lastSeg[lastSeg.length 1],

        nextLastPoint = lastSeg[lastSeg.length 2],

        angle = Math.atan((lastPoint.plotX nextLastPoint.plotX) /

        (lastPoint.plotY nextLastPoint.plotY)),

        path = [];

Then, the arrow is created as an SVG path:

    path.push(‘M’, lastPoint.plotX, lastPoint.plotY);

    path.push(

        ‘L’,

        lastPoint.plotX + arrowWidth * Math.cos(angle),

        lastPoint.plotY arrowWidth * Math.sin(angle)

    );

    path.push(

        lastPoint.plotX + arrowLength * Math.sin(angle),

        lastPoint.plotY + arrowLength * Math.cos(angle)

    );

    path.push(

        lastPoint.plotX arrowWidth * Math.cos(angle),

        lastPoint.plotY + arrowWidth * Math.sin(angle),

        ‘Z’

    );

And finally, the new functionality is added in by pushing the arrow path into the series:

    series.chart.renderer.path(path)

    .attr({

        fill: series.color

    }).add(series.group);

And there you have it! An arrow in the graph, sitting on top of the last point in the series. 

 

The keyword, however, is sitting; the arrow won’t move! When we resize the window, the chart redraws itself, creating a new arrow at a new position… but the old path is left behind. It becomes a ghost arrow, unmanaged by Highcharts.

Well we don’t want that — what we need is a real marker, not just an overlaid graphic. Highcharts had the right idea: hijacking a rendering function and inserting new functionality. We just need to find the right function to hijack.

 

Searching for “markers” in the Highcharts source brings up a promising lead, Highcharts.Series.prototype.drawPoints:

    /*

     * Draw the markers

     */

    drawPoints: function () {

    …

    …

    }

This looks like the function that a Highcharts.Series object would use to create its markers. There’s a lot of code here, so lets go to the deepest block, where we’ll find the calls to the renderer:

    if (graphic) { // update an existing marker

        graphic.animate(extend({

            x: plotX radius,

            y: plotY radius

        }, graphic.symbolName ? {

            width: 2 * radius,

            height: 2 * radius

        } : {}));

    } else if (radius > 0 || isImage) { // create a new marker

        point.graphic = chart.renderer.symbol(

            symbol,

            plotX radius,

            plotY radius,

            2 * radius,

            2 * radius

        )

        .attr(pointAttr)

        .add(series.group);

    }

This is where the markers are updated/created. If we already have a marker defined, we animate it by moving it to its new position. If we don’t, we create a new marker by calling chart.renderer.symbol and then add the marker to the series. We’ll need to inject our code here. 

It looks like the renderer.symbol function is taking our symbol name as its first parameter, followed by positioning coordinates. Let’s find the symbol function, which lives at Highcharts.Renderer.prototype.symbol:

    symbol: function (symbol, x, y, width, height, options) {

        var obj,

        // get the symbol definition function

        symbolFn = this.symbols[symbol],

        // check if there’s a path defined for this symbol

        path = symbolFn && symbolFn(

            mathRound(x),

            mathRound(y),

            width,

            height,

            options

        ),…

The symbol function is looking at its internal array of symbol generators (found in Highcharts.Renderer.prototype.symbols), pulling out the specified one, and creating an SVG path using that generator. The symbol generator name corresponds to the symbol names we can pass in to chart options, like ‘circle’, ‘triangle’, ‘diamond’, etc.

Let’s define a new one called ‘arrow’ and inject it into the list of generators:

    Highcharts.Renderer.prototype.symbols.arrow = function(x,y,w,h,options){

        var angle;

        angle = options.angle; // angle for arrow

        w = w/1.2;

        x = x + h/2 2*w*Math.sin(angle);

        y = y + h/2 2*w*Math.cos(angle);

        h = w * 2;

        return [‘M’,x,y,

                ‘L’,x + w*Math.cos(angle),y w*Math.sin(angle),

                 x + h*Math.sin(angle),y + h*Math.cos(angle),

                 x w*Math.cos(angle),y + w*Math.sin(angle),

                 ‘Z’];

        };

Now, all we need to do is generate an angle and pass it into our rendering function. We can do this by adding a few lines of code into the loop inside drawPoints:

    lastPoint = points[i1];

    thisPoint = points[i];

    theta = Math.atan( (plotX lastPoint.plotX) / (plotY lastPoint.plotY) );

… and then passing theta into chart.renderer.symbol:

    point.graphic = chart.renderer.symbol(

        symbol,

        plotX radius,

        plotY radius,

        2 * radius,

        2 * radius,

        {angle: theta})

    .attr(pointAttr).

    .add(series.group);

That’s it! Now we have our own custom marker symbol called ‘arrow’. It has the full functionality of a standard marker: it can be resized and set on a point-to-point basis, and it responds correctly to series redrawing.

Coming soon to an IS2 report near you… but for now, check it out here.

 

Happy Haxing, and hope this post provides some direction to future Highcharts developers! 🙂