Extending Highcharts

Since version 2.3, Highcharts is built in a modular way with extensions in mind. 

  • Major chart concepts correspond to JavaScript prototypes or "classes" which are exposed on the Highcharts namespace and can easily be modified. Examples are Highcharts.Series, Highcharts.Tooltip, Highcharts.Chart, Highcharts.Axis, Highcharts.Legend etc. Check full list of classes.
  • Constructor logic is consequently kept in a method, init, to allow overriding the initialization.
  • Events can be added with 'addEvent': Highcharts.addEvent(chart, 'load', someFunction);
  • Some, but not all, prototypes and properties are listed at api.highcharts.com. Some prototypes and properties are not listed, which means they may change in future versions as we optimize and adapt the library. We do not discourage using these members, but warn that your plugin should be tested with future versions of Highcharts. These members can be identified by inspecting the Highcharts namespace as well as generated chart objects in developer tools, and by studying the source code of highcharts.src.js.

Wrapping up a plugin

Highcharts plugins should be wrapped in an anonymous self-executing function in order to prevent variable pollution to the global scope. A good practice is to wrap plugins like this:

(function (H) {
const { Chart, Series } = H; // shortcuts to Highcharts classes
let localVar; // local variable
doSomething();
}(Highcharts));

Initializing an extension when the chart initializes

Events can be added to both a class and an instance. In order to add a general listener to initialize the extension on every chart, an event can be added to the Chart class.

H.addEvent(H.Chart, 'load', function(e) {
const chart = e.target;
H.addEvent(chart.container, 'click', function(e) {
e = chart.pointer.normalize(e);
console.log(`Clicked chart at ${e.chartX}, ${e.chartY}`);
});
H.addEvent(chart.xAxis[0], 'afterSetExtremes', function(e) {
console.log(`Set extremes to ${e.min}, ${e.max}`);
});
});

Try it live

Wrapping prototype functions

JavaScript with its dynamic nature is extremely powerful when it comes to altering the behaviour of scripts on the fly. In Highcharts we created a utility called wrap, which wraps an existing prototype function ("method") and allows you to add your own code before or after it. 

The wrap function accepts the parent object as the first argument, the name of the function to wrap as the second, and a callback replacement function as the third. The original function is passed as the first argument to the replacement function, and original arguments follow after that.

It's best explained by a code sample:

H.wrap(H.Series.types.line.prototype, 'drawGraph', function (proceed) {
// Before the original function
console.log("We are about to draw the graph: ", typeof this.graph);
// Now apply the original function with the original arguments,
// which are sliced off this function's arguments
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
// Add some code after the original function
console.log("We just finished drawing the graph: ", typeof this.graph);
});

Try it live

Since v10 all module functions are available through a window event, HighchartsModuleLoaded. This includes utility functions and members that are not deliberately exposed. It is a powerful API that was created with the intention of making it easier for the Highcharts developers and support team to provide temporary workarounds for bugs, or for special client requests. It should be warned that the module paths are not canonical, and may be subject to change as the product evolves. The HighchartsModuleLoaded event handlers must be defined before Highcharts is loaded. When loading ES modules, this will not work, and is also not necessary because modules can be accessed directly.

window.addEventListener('HighchartsModuleLoaded', function(e) {
if (e.detail.path === 'Core/FormatUtilities.js') {
// The original function
const numberFormat = e.detail.module.numberFormat;
// A stupid proof of concept - modify all formatted numbers
e.detail.module.numberFormat = function () {
const n = numberFormat.apply(this, arguments);
return '~' + n;
}
}
});

Try it live

Example extension

In this example the client wanted to use markers ("trackballs") on column type series in Highcharts Stock. Markers is currently only supported in line type series. To get this functionality, a small plugin can be written.

This plugin will add a trackball to each series in the chart, that does not already support and contain a marker.

To gain this we start with the following code, creating a self-executing function to contain the plugin:

(function (H) {
// This is a self executing function
// The global variable Highcharts is passed along with a reference H
}(Highcharts));

Afterwards, we need to add extra functionality to the methods Tooltip.prototype.refresh and Tooltip.prototype.hide. For this, we wrap the methods:

(function (H) {
H.wrap(H.Tooltip.prototype, 'refresh', function (proceed, points) {
// When refresh is called, code inside this wrap is executed
});
}(Highcharts));

When refresh is called, we want it to draw a trackball on the current point in each series. If a series already contains a marker this function should be dropped.

H.wrap(H.Tooltip.prototype, 'refresh', function (proceed, points) {
// Run the original proceed method
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
// For each point add or update trackball
H.each(points, function (point) {
// Function variables
var series = point.series,
chart = series.chart,
pointX = point.plotX + series.xAxis.pos,
pointY = H.pick(point.plotClose, point.plotY) + series.yAxis.pos;
// If trackball functionality does not already exist
if (!series.options.marker) {
// If trackball is not defined
if (!series.trackball) {
// Creates a new trackball with same color as the series
series.trackball = chart.renderer.circle(pointX, pointY, 5).attr({
fill: series.color,
stroke: 'white',
'stroke-width': 1,
zIndex: 5
}).add();
} else {
// Updates the position of the trackball
series.trackball.attr({
x: pointX,
y: pointY
});
}
}
});
});

Now the trackball will be displayed, but we also need to hide it when the tooltip is removed. Therefore som extra functionality is also needed in the hide method. A new wrap is added inside the function containing the plugin:

H.wrap(H.Tooltip.prototype, 'hide', function (proceed) {
var series = this.chart.series;
// Run original proceed method
proceed.apply(this);
// For each series destroy trackball
H.each(series, function (serie) {
var trackball = serie.trackball;
if (trackball) {
serie.trackball = trackball.destroy();
}
});
});

That was all, the whole sample can be viewed in jsFiddle.