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 classeslet localVar; // local variabledoSomething();}(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}`);});});
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 functionconsole.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 argumentsproceed.apply(this, Array.prototype.slice.call(arguments, 1));// Add some code after the original functionconsole.log("We just finished drawing the graph: ", typeof this.graph);});
When loading ES modules, one can access modules directly for further modifications.
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 methodproceed.apply(this, Array.prototype.slice.call(arguments, 1));// For each point add or update trackballH.each(points, function (point) {// Function variablesvar 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 existif (!series.options.marker) {// If trackball is not definedif (!series.trackball) {// Creates a new trackball with same color as the seriesseries.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 trackballseries.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 methodproceed.apply(this);// For each series destroy trackballH.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.