highcharts-custom-events
Version:
Plugin that allows you to add extra events, like double / right click on each chart element, for Highcharts.
619 lines (526 loc) • 18.2 kB
JavaScript
/**
* Custom events v3.0.10 (2020-12-21)
*
* (c) 2012-2020 Black Label
*
* License: Creative Commons Attribution (CC)
*/
/* global Highcharts setTimeout clearTimeout module:true */
/* eslint no-loop-func: 0 */
/**
* @namespace customEvents
**/
(function (factory) {
if (typeof module === 'object' && module.exports) {
factory['default'] = factory;
module.exports = factory;
} else if (typeof define === 'function' && define.amd) {
define('highcharts-custom-events', ['highcharts'], function (Highcharts) {
factory(Highcharts);
factory.Highcharts = Highcharts;
return factory;
});
} else {
factory(Highcharts);
}
}(function (HC) {
/* global Highcharts :true, window */
'use strict';
var UNDEFINED,
DBLCLICK = 'dblclick',
TOUCHSTART = 'touchstart',
CLICK = 'click',
pick = HC.pick,
wrap = HC.wrap,
merge = HC.merge,
addEvent = HC.addEvent,
isTouchDevice = HC.isTouchDevice,
defaultOptions = HC.getOptions().plotOptions,
axisProto = HC.Axis && HC.Axis.prototype,
plotLineOrBandProto = HC.PlotLineOrBand && HC.PlotLineOrBand.prototype,
seriesTypes = HC.seriesTypes,
seriesProto = HC.Series && HC.Series.prototype,
isObject,
isNumber,
isArray,
objectEach,
customEvents,
proto,
methods;
/*
* Back compatibility
*/
isArray = HC.isArray || function (obj) {
var str = Object.prototype.toString.call(obj);
return str === '[object Array]' || str === '[object Array Iterator]';
};
objectEach = HC.objectEach || function (obj, fn, ctx) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
fn.call(ctx, obj[key], key, obj);
}
}
};
isNumber = HC.isNumber || function (n) {
return typeof n === 'number' && !isNaN(n);
};
isObject = HC.isObject || function (obj) {
return obj && typeof obj === 'object';
}
/**
* WRAPPED FUNCTIONS
*/
// reset exis events
if (plotLineOrBandProto) { // # condition for highmaps and custom builds
wrap(plotLineOrBandProto, 'render', function (proceed) {
var defaultEvents = this.options && this.options.events;
// reset default events on plot lines or bands
if (defaultEvents) {
defaultEvents = false;
}
return proceed.apply(this, Array.prototype.slice.call(arguments, 1));
});
}
if (seriesProto) { // # condition for highmaps and custom builds
wrap(seriesProto, 'init', function (proceed, chart, options) {
var chartOptions = chart.options,
plotOptions = chartOptions.plotOptions,
seriesOptions = chartOptions.plotOptions.series,
userOptions = merge(seriesOptions, plotOptions[this.type], options),
userOptionsEvents = userOptions && userOptions.events,
userOptionsPointEvents = userOptions && userOptions.point && userOptions.point.events; // Fixed #70
// reset default events on series and series point
options.events = {};
options.point = {
events: {}
};
// Add support for legendItemClick
if (userOptionsEvents) {
options.events = {
legendItemClick: userOptionsEvents && userOptionsEvents.legendItemClick
};
}
// Add support for legendItemClick in pie chart
if (userOptionsPointEvents) {
options.point.events = {
legendItemClick: userOptionsPointEvents && userOptionsPointEvents.legendItemClick,
select: userOptionsPointEvents && userOptionsPointEvents.select,
unselect: userOptionsPointEvents && userOptionsPointEvents.unselect,
click: userOptionsPointEvents && userOptionsPointEvents.click,
mouseOut: userOptionsPointEvents && userOptionsPointEvents.mouseOut,
mouseOver: userOptionsPointEvents && userOptionsPointEvents.mouseOver
};
}
// attach events to custom object, which is used in attach event
options.customEvents = {
series: userOptionsEvents,
point: userOptionsPointEvents
};
// call default action
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
});
}
HC.Chart.prototype.customEvent = {
/**
* @description Example: [HC.Series, ['drawPoints', 'drawDataLabels']]
* @memberof customEvents
* @returns {Array} array of pairs: prototype, array of methods to wrap
**/
getEventsProtoMethods: function () {
var protoMethods = [
[HC.Tick, ['addLabel']],
[HC.Axis, ['render']],
[HC.Axis, ['drawCrosshair']],
[HC.Chart, ['setTitle']],
[HC.Legend, ['renderItem']],
[HC.PlotLineOrBand, ['render']],
[HC.Series, ['drawPoints', 'drawDataLabels']]
];
// support for extra series
objectEach(seriesTypes, function (fn, seriesType) {
protoMethods.push([
seriesTypes[seriesType], ['drawPoints', 'drawDataLabels']
]);
});
return protoMethods;
},
/**
* @description Init method, based on getEventsProtoMethods() array. Iterates on array of prototypes and methods to wrap
* @memberof customEvents
**/
init: function () {
var eventsProtoMethods = this.getEventsProtoMethods(); // array of pairs [object, [methods]]
eventsProtoMethods.forEach(function (protoMethod) {
if (isArray(protoMethod)) {
proto = protoMethod[0] && protoMethod[0].prototype;
methods = protoMethod[1];
if (proto) {
methods.forEach(function (method) {
customEvents.attach(proto, method);
});
}
}
});
},
/**
* @description Wraps methods i.e drawPoints to extract SVG element and set an event by calling customEvents.add()
* @param {Object} proto Highcharts prototype i.e Highcharts.Series.prototype
* @param {Object} hcMethod name of wrapped method i.e drawPoints
* @memberof customEvents
**/
attach: function (proto, hcMethod) {
wrap(proto, hcMethod, function (proceed, param) {
var eventElement = {
events: UNDEFINED,
element: UNDEFINED
},
proceedObject,
len,
j;
// call default actions
proceedObject = proceed.apply(this, Array.prototype.slice.call(arguments, 1));
// call
eventElement = customEvents.eventElement[hcMethod].call(this, param);
// stop, when events and SVG element do not exist
if (!eventElement.events && !eventElement.eventsSubtitle && !eventElement.eventsPoint) {
return proceedObject;
}
if (eventElement.eventsPoint) {
len = eventElement.elementPoint.length;
// attach events per each point
for (j = 0; j < len; j++) {
if (eventElement.elementPoint[j]) {
var elemPoint = pick(eventElement.elementPoint[j].graphic, eventElement.elementPoint[j]);
if (elemPoint && elemPoint !== UNDEFINED) {
customEvents.add(elemPoint, eventElement.eventsPoint, eventElement.elementPoint[j], eventElement, true);
}
}
}
}
// attach event to subtitle
if (eventElement.eventsSubtitle) {
customEvents.add(eventElement.elementSubtitle, eventElement.eventsSubtitle, eventElement, this);
}
// attach event to each datalabal and add reference to each point
if (eventElement.dataLabelsPoints) {
var dataLabelsPoints = eventElement.dataLabelsPoints;
len = dataLabelsPoints.length;
for (j = 0; j < len; j++) {
customEvents.add(
dataLabelsPoints[j].dataLabel,
eventElement.events,
{
dataLabel: dataLabelsPoints[j].dataLabel,
point: dataLabelsPoints[j]
},
this
);
}
}
// attach event to stackLabels
if (eventElement.eventsStackLabel) {
customEvents.add(eventElement.elementStackLabel, eventElement.eventsStackLabel, eventElement, this);
}
customEvents.add(eventElement.element, eventElement.events, eventElement, this);
return proceedObject;
});
},
/**
* @description adds event on a SVG element
* @param {Object} SVGelem graphic element
* @param {Object} events object with all events
* @param {Object} elemObj "this" object, which is available in the event
* @param {Object} series chart series
* @memberof customEvents
**/
add: function (SVGelem, events, elemObj, eventElement, isPoint) {
var eventObject = eventElement.eventObject || elemObj.eventObject, // Fix series reference #89
isSeries = elemObj.isSeries || eventElement.isSeries;
// stop when SVG element does not exist
if (!SVGelem || !SVGelem.element) {
return false;
}
for (var action in events) {
(function (event) {
if (events.hasOwnProperty(event) && !SVGelem[event]) {
if (isTouchDevice && event === DBLCLICK) { // #30 - fallback for iPad
var tapped = false;
addEvent(SVGelem.element, TOUCHSTART, function (e) {
e.preventDefault();
if (isSeries) { // #93
var chart = eventObject.chart,
normalizedEvent = chart.pointer.normalize(e);
if (!eventObject.directTouch) {
elemObj = eventObject.searchPoint(normalizedEvent, eventObject.kdDimensions === 1);
}
e.point = elemObj; // #89 point reference in mouse event
}
if ((eventObject && !isPoint) || (eventObject && isNumber(eventObject.value))) { // #95
eventObject.value = elemObj.textStr;
elemObj = eventObject;
}
if (elemObj && elemObj.textStr) { // labels
elemObj.value = elemObj.textStr;
}
if (elemObj && elemObj.drilldown) { // #114 - drillUp - undefined ddDupes []
elemObj.doDrilldown(undefined, undefined, e);
}
if (!tapped) {
tapped = setTimeout(function () {
tapped = null;
if (events[CLICK] && elemObj && elemObj.drilldown === undefined) {
events[CLICK].call(elemObj, e); // call single click action
}
}, 300);
} else {
clearTimeout(tapped);
tapped = null;
if (elemObj && elemObj.drilldown === undefined) {
events[event].call(elemObj, e);
}
}
return false;
});
} else {
addEvent(SVGelem.element, event, function (e) {
e.stopPropagation();
if (isSeries) { // #108, #93 - references in e.point and this after chart.update()
var chart = eventObject.chart,
normalizedEvent = chart.pointer.normalize(e);
if (!eventObject.directTouch) {
elemObj = eventObject.searchPoint(normalizedEvent, eventObject.kdDimensions === 1);
}
e.point = elemObj; // #89 point reference in mouse event
}
if ((eventObject && !isPoint) || (eventObject && isNumber(eventObject.value))) { // #95 wrong reference for axis labels
eventObject.value = elemObj.textStr || eventObject.value; // #95, #98 wrong reference for axis labels
elemObj = eventObject;
}
if (elemObj && elemObj.textStr) { // labels
elemObj.value = elemObj.textStr;
}
if (
isPoint &&
event === 'click' &&
elemObj.series &&
elemObj.series.options &&
elemObj.series.options.allowPointSelect
) {
var defaultFunction = function (event) {
// Control key is for Windows, meta (= Cmd key) for Mac, Shift
// for Opera.
if (elemObj.select) { // #2911
elemObj.select(null, e.ctrlKey || e.metaKey || e.shiftKey);
}
};
HC.fireEvent(elemObj, event, e, defaultFunction);
}
if (elemObj && elemObj.drilldown) { // #114 - drillUp - undefined ddDupes []
elemObj.doDrilldown(undefined, undefined, e);
} else if (events && events[event]) {
events[event].call(elemObj, e);
}
return false;
});
}
SVGelem[event] = function () {
return true;
};
}
})(action);
}
},
eventElement: {
/**
* @typedef {Object} eventElement
**/
/**
* @description Extracts SVG elements from points
* @property {Object} eventsPoint events for point
* @property {Array} elementPoint array of SVG point elements
* @property {Object} eventObject object kept in this on i.e click
* @return {Object} { events: object, element: object }
* @memberof customEvents
**/
addLabel: function () {
var parent = this.parent,
axis = this.axis,
axisOptions = axis.options,
eventsPoint = axisOptions.labels && axisOptions.labels.events,
elementPoint = [this.label],
len, i;
if (parent) {
var step = this; // current label
while (step) {
if (isArray(step)) {
len = step.length;
for (i = 0; i < len; i++) {
elementPoint.push(step[i].label);
}
} else {
elementPoint.push(step.label);
}
step = step.parent;
}
}
return {
eventsPoint: eventsPoint,
elementPoint: elementPoint,
eventObject: {
axis: axis,
isFirst: this.isFirst,
isLast: this.isLast,
chart: axis.chart,
dateTimeLabelFormat: axisOptions.dateTimeLabelFormats,
value: this.pos,
pos: this.pos
}
};
},
/**
* @description Extracts SVG elements from title and subtitle
* @property {Object} events events for title
* @property {Array} elementPoint title SVG element
* @property {Object} eventsSubtitle events for subtitle
* @property {Array} elementSubtitle subtitle SVG element
* @return {Object} {event: object, element: object, eventsSubtitle: object, elementSubtitle: object }
* @memberof customEvents
**/
setTitle: function () {
var events = this.options.title && this.options.title.events,
element = this.title,
eventsSubtitle = this.options.subtitle && this.options.subtitle.events,
elementSubtitle = this.subtitle;
return {
events: events,
element: element,
eventsSubtitle: eventsSubtitle,
elementSubtitle: elementSubtitle
};
},
/**
* @description Extracts SVG elements from dataLabels
* @property {Object} events events for dataLabels
* @property {Array} element dataLabels SVG element
* @return {Object} { events: object, element: object }
* @memberof customEvents
**/
drawDataLabels: function () {
var dataLabelsGroup = this.dataLabelsGroup,
dataLabelEvents = dataLabelsGroup && this.options.dataLabels.events;
return {
events: dataLabelEvents,
element: dataLabelsGroup,
dataLabelsPoints: this.points
};
},
/**
* @description Extracts SVG elements from axis title and stackLabels
* @property {Object} events events for axis title
* @property {Array} element axis title SVG element
* @property {Object} eventsPoint events for stacklabels
* @property {Array} elementPoint stacklabels SVG element
* @property {Object} eventsStackLabel events for stacklabels
* @property {Array} elementStackLabel stacklabels group SVG element
* @return {Object} { events: object, element: object, eventsPoint: object, elementPoint: object, eventsStackLabel: object, elementStackLabel: object }
* @memberof customEvents
**/
render: function () {
var stackLabels = this.options.stackLabels,
events,
element,
eventsPoint,
elementPoint,
eventsStackLabel,
elementStackLabel;
if (this.axisTitle) {
events = this.options.title.events;
element = this.axisTitle;
}
if (stackLabels && stackLabels.enabled) {
eventsPoint = stackLabels.events;
elementPoint = this.stacks || this.stacking.stacks;
eventsStackLabel = stackLabels.events;
elementStackLabel = this.stackTotalGroup || this.stacking.stackTotalGroup;
}
return {
events: events,
element: element,
eventsPoint: eventsPoint,
elementPoint: elementPoint,
eventsStackLabel: eventsStackLabel,
elementStackLabel: elementStackLabel
};
},
/**
* @description Extracts SVG elements from series and series points
* @property {Object} events events for series
* @property {Array} element series SVG element
* @property {Object} events events for series points
* @property {Array} element series points SVG element
* @return {Object} { events: object, element: object, eventsPoint: object, elementPoint: object }
* @memberof customEvents
**/
drawPoints: function () {
var op = this.options,
type = this.type,
events = op.customEvents ? op.customEvents.series : op.events,
element = this.group,
eventsPoint = op.customEvents ? op.customEvents.point : op.point.events,
elementPoint;
if (defaultOptions[type] && defaultOptions[type].marker && !this.bubblePadding) {
elementPoint = [this.markerGroup]; // get markers when enabled
} else {
elementPoint = this.points; // extract points
}
if (!this.kdTree && !this.buildingKdTree) {
this.buildKDTree(); // #86, missing reference to point on first mouseover
}
return {
events: events,
element: element,
eventsPoint: eventsPoint,
elementPoint: elementPoint,
eventObject: this,
isSeries: true
};
},
/**
* @description Extracts SVG elements from legend item
* @property {Object} events events for legend item
* @property {Array} element legend item SVG element
* @return {Object} { events: object, element: object }
* @memberof customEvents
**/
renderItem: function (item) {
return {
events: this.options.itemEvents,
element: item.legendGroup,
eventObject: item
};
},
/**
* @description Extracts SVG elements from crosshair
* @property {Object} events events for crosshair
* @property {Array} element crosshair SVG element
* @return {Object} { events: object, element: object }
* @memberof customEvents
**/
drawCrosshair: function () {
var cross = this.cross,
crosshairOptions = this.options.crosshair;
if (cross) {
cross.css({
'pointer-events': 'auto'
});
}
return {
events: crosshairOptions && crosshairOptions.events,
element: cross
};
}
}
};
customEvents = HC.Chart.prototype.customEvent;
customEvents.init();
}));