1 | /**
|
2 | * @license Highcharts JS v5.0.0 (2016-09-29)
|
3 | * Boost module
|
4 | *
|
5 | * (c) 2010-2016 Highsoft AS
|
6 | * Author: Torstein Honsi
|
7 | *
|
8 | * License: www.highcharts.com/license
|
9 | */
|
10 | (function(factory) {
|
11 | if (typeof module === 'object' && module.exports) {
|
12 | module.exports = factory;
|
13 | } else {
|
14 | factory(Highcharts);
|
15 | }
|
16 | }(function(Highcharts) {
|
17 | (function(H) {
|
18 | /**
|
19 | * License: www.highcharts.com/license
|
20 | * Author: Torstein Honsi
|
21 | *
|
22 | * This is an experimental Highcharts module that draws long data series on a canvas
|
23 | * in order to increase performance of the initial load time and tooltip responsiveness.
|
24 | *
|
25 | * Compatible with HTML5 canvas compatible browsers (not IE < 9).
|
26 | *
|
27 | *
|
28 | *
|
29 | * Development plan
|
30 | * - Column range.
|
31 | * - Heatmap.
|
32 | * - Treemap.
|
33 | * - Check how it works with Highstock and data grouping. Currently it only works when navigator.adaptToUpdatedData
|
34 | * is false. It is also recommended to set scrollbar.liveRedraw to false.
|
35 | * - Check inverted charts.
|
36 | * - Check reversed axes.
|
37 | * - Chart callback should be async after last series is drawn. (But not necessarily, we don't do
|
38 | that with initial series animation).
|
39 | * - Cache full-size image so we don't have to redraw on hide/show and zoom up. But k-d-tree still
|
40 | * needs to be built.
|
41 | * - Test IE9 and IE10.
|
42 | * - Stacking is not perhaps not correct since it doesn't use the translation given in
|
43 | * the translate method. If this gets to complicated, a possible way out would be to
|
44 | * have a simplified renderCanvas method that simply draws the areaPath on a canvas.
|
45 | *
|
46 | * If this module is taken in as part of the core
|
47 | * - All the loading logic should be merged with core. Update styles in the core.
|
48 | * - Most of the method wraps should probably be added directly in parent methods.
|
49 | *
|
50 | * Notes for boost mode
|
51 | * - Area lines are not drawn
|
52 | * - Point markers are not drawn on line-type series
|
53 | * - Lines are not drawn on scatter charts
|
54 | * - Zones and negativeColor don't work
|
55 | * - Columns are always one pixel wide. Don't set the threshold too low.
|
56 | *
|
57 | * Optimizing tips for users
|
58 | * - For scatter plots, use a marker.radius of 1 or less. It results in a rectangle being drawn, which is
|
59 | * considerably faster than a circle.
|
60 | * - Set extremes (min, max) explicitly on the axes in order for Highcharts to avoid computing extremes.
|
61 | * - Set enableMouseTracking to false on the series to improve total rendering time.
|
62 | * - The default threshold is set based on one series. If you have multiple, dense series, the combined
|
63 | * number of points drawn gets higher, and you may want to set the threshold lower in order to
|
64 | * use optimizations.
|
65 | */
|
66 |
|
67 | ;
|
68 |
|
69 | var win = H.win,
|
70 | doc = win.document,
|
71 | noop = function() {},
|
72 | Color = H.Color,
|
73 | Series = H.Series,
|
74 | seriesTypes = H.seriesTypes,
|
75 | each = H.each,
|
76 | extend = H.extend,
|
77 | addEvent = H.addEvent,
|
78 | fireEvent = H.fireEvent,
|
79 | grep = H.grep,
|
80 | isNumber = H.isNumber,
|
81 | merge = H.merge,
|
82 | pick = H.pick,
|
83 | wrap = H.wrap,
|
84 | plotOptions = H.getOptions().plotOptions,
|
85 | CHUNK_SIZE = 50000,
|
86 | destroyLoadingDiv;
|
87 |
|
88 | function eachAsync(arr, fn, finalFunc, chunkSize, i) {
|
89 | i = i || 0;
|
90 | chunkSize = chunkSize || CHUNK_SIZE;
|
91 |
|
92 | var threshold = i + chunkSize,
|
93 | proceed = true;
|
94 |
|
95 | while (proceed && i < threshold && i < arr.length) {
|
96 | proceed = fn(arr[i], i);
|
97 | i = i + 1;
|
98 | }
|
99 | if (proceed) {
|
100 | if (i < arr.length) {
|
101 | setTimeout(function() {
|
102 | eachAsync(arr, fn, finalFunc, chunkSize, i);
|
103 | });
|
104 | } else if (finalFunc) {
|
105 | finalFunc();
|
106 | }
|
107 | }
|
108 | }
|
109 |
|
110 | // Set default options
|
111 | each(['area', 'arearange', 'column', 'line', 'scatter'], function(type) {
|
112 | if (plotOptions[type]) {
|
113 | plotOptions[type].boostThreshold = 5000;
|
114 | }
|
115 | });
|
116 |
|
117 | /**
|
118 | * Override a bunch of methods the same way. If the number of points is below the threshold,
|
119 | * run the original method. If not, check for a canvas version or do nothing.
|
120 | */
|
121 | each(['translate', 'generatePoints', 'drawTracker', 'drawPoints', 'render'], function(method) {
|
122 | function branch(proceed) {
|
123 | var letItPass = this.options.stacking && (method === 'translate' || method === 'generatePoints');
|
124 | if ((this.processedXData || this.options.data).length < (this.options.boostThreshold || Number.MAX_VALUE) ||
|
125 | letItPass) {
|
126 |
|
127 | // Clear image
|
128 | if (method === 'render' && this.image) {
|
129 | this.image.attr({
|
130 | href: ''
|
131 | });
|
132 | this.animate = null; // We're zooming in, don't run animation
|
133 | }
|
134 |
|
135 | proceed.call(this);
|
136 |
|
137 | // If a canvas version of the method exists, like renderCanvas(), run
|
138 | } else if (this[method + 'Canvas']) {
|
139 |
|
140 | this[method + 'Canvas']();
|
141 | }
|
142 | }
|
143 | wrap(Series.prototype, method, branch);
|
144 |
|
145 | // A special case for some types - its translate method is already wrapped
|
146 | if (method === 'translate') {
|
147 | if (seriesTypes.column) {
|
148 | wrap(seriesTypes.column.prototype, method, branch);
|
149 | }
|
150 | if (seriesTypes.arearange) {
|
151 | wrap(seriesTypes.arearange.prototype, method, branch);
|
152 | }
|
153 | }
|
154 | });
|
155 |
|
156 | /**
|
157 | * Do not compute extremes when min and max are set.
|
158 | * If we use this in the core, we can add the hook to hasExtremes to the methods directly.
|
159 | */
|
160 | wrap(Series.prototype, 'getExtremes', function(proceed) {
|
161 | if (!this.hasExtremes()) {
|
162 | proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
163 | }
|
164 | });
|
165 | wrap(Series.prototype, 'setData', function(proceed) {
|
166 | if (!this.hasExtremes(true)) {
|
167 | proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
168 | }
|
169 | });
|
170 | wrap(Series.prototype, 'processData', function(proceed) {
|
171 | if (!this.hasExtremes(true)) {
|
172 | proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
173 | }
|
174 | });
|
175 |
|
176 |
|
177 | H.extend(Series.prototype, {
|
178 | pointRange: 0,
|
179 | allowDG: false, // No data grouping, let boost handle large data
|
180 | hasExtremes: function(checkX) {
|
181 | var options = this.options,
|
182 | data = options.data,
|
183 | xAxis = this.xAxis && this.xAxis.options,
|
184 | yAxis = this.yAxis && this.yAxis.options;
|
185 | return data.length > (options.boostThreshold || Number.MAX_VALUE) && isNumber(yAxis.min) && isNumber(yAxis.max) &&
|
186 | (!checkX || (isNumber(xAxis.min) && isNumber(xAxis.max)));
|
187 | },
|
188 |
|
189 | /**
|
190 | * If implemented in the core, parts of this can probably be shared with other similar
|
191 | * methods in Highcharts.
|
192 | */
|
193 | destroyGraphics: function() {
|
194 | var series = this,
|
195 | points = this.points,
|
196 | point,
|
197 | i;
|
198 |
|
199 | if (points) {
|
200 | for (i = 0; i < points.length; i = i + 1) {
|
201 | point = points[i];
|
202 | if (point && point.graphic) {
|
203 | point.graphic = point.graphic.destroy();
|
204 | }
|
205 | }
|
206 | }
|
207 |
|
208 | each(['graph', 'area', 'tracker'], function(prop) {
|
209 | if (series[prop]) {
|
210 | series[prop] = series[prop].destroy();
|
211 | }
|
212 | });
|
213 | },
|
214 |
|
215 | /**
|
216 | * Create a hidden canvas to draw the graph on. The contents is later copied over
|
217 | * to an SVG image element.
|
218 | */
|
219 | getContext: function() {
|
220 | var chart = this.chart,
|
221 | width = chart.plotWidth,
|
222 | height = chart.plotHeight,
|
223 | ctx = this.ctx,
|
224 | swapXY = function(proceed, x, y, a, b, c, d) {
|
225 | proceed.call(this, y, x, a, b, c, d);
|
226 | };
|
227 |
|
228 | if (!this.canvas) {
|
229 | this.canvas = doc.createElement('canvas');
|
230 | this.image = chart.renderer.image('', 0, 0, width, height).add(this.group);
|
231 | this.ctx = ctx = this.canvas.getContext('2d');
|
232 | if (chart.inverted) {
|
233 | each(['moveTo', 'lineTo', 'rect', 'arc'], function(fn) {
|
234 | wrap(ctx, fn, swapXY);
|
235 | });
|
236 | }
|
237 | } else {
|
238 | ctx.clearRect(0, 0, width, height);
|
239 | }
|
240 |
|
241 | this.canvas.width = width;
|
242 | this.canvas.height = height;
|
243 | this.image.attr({
|
244 | width: width,
|
245 | height: height
|
246 | });
|
247 |
|
248 | return ctx;
|
249 | },
|
250 |
|
251 | /**
|
252 | * Draw the canvas image inside an SVG image
|
253 | */
|
254 | canvasToSVG: function() {
|
255 | this.image.attr({
|
256 | href: this.canvas.toDataURL('image/png')
|
257 | });
|
258 | },
|
259 |
|
260 | cvsLineTo: function(ctx, clientX, plotY) {
|
261 | ctx.lineTo(clientX, plotY);
|
262 | },
|
263 |
|
264 | renderCanvas: function() {
|
265 | var series = this,
|
266 | options = series.options,
|
267 | chart = series.chart,
|
268 | xAxis = this.xAxis,
|
269 | yAxis = this.yAxis,
|
270 | ctx,
|
271 | c = 0,
|
272 | xData = series.processedXData,
|
273 | yData = series.processedYData,
|
274 | rawData = options.data,
|
275 | xExtremes = xAxis.getExtremes(),
|
276 | xMin = xExtremes.min,
|
277 | xMax = xExtremes.max,
|
278 | yExtremes = yAxis.getExtremes(),
|
279 | yMin = yExtremes.min,
|
280 | yMax = yExtremes.max,
|
281 | pointTaken = {},
|
282 | lastClientX,
|
283 | sampling = !!series.sampling,
|
284 | points,
|
285 | r = options.marker && options.marker.radius,
|
286 | cvsDrawPoint = this.cvsDrawPoint,
|
287 | cvsLineTo = options.lineWidth ? this.cvsLineTo : false,
|
288 | cvsMarker = r <= 1 ? this.cvsMarkerSquare : this.cvsMarkerCircle,
|
289 | enableMouseTracking = options.enableMouseTracking !== false,
|
290 | lastPoint,
|
291 | threshold = options.threshold,
|
292 | yBottom = yAxis.getThreshold(threshold),
|
293 | hasThreshold = isNumber(threshold),
|
294 | translatedThreshold = yBottom,
|
295 | doFill = this.fill,
|
296 | isRange = series.pointArrayMap && series.pointArrayMap.join(',') === 'low,high',
|
297 | isStacked = !!options.stacking,
|
298 | cropStart = series.cropStart || 0,
|
299 | loadingOptions = chart.options.loading,
|
300 | requireSorting = series.requireSorting,
|
301 | wasNull,
|
302 | connectNulls = options.connectNulls,
|
303 | useRaw = !xData,
|
304 | minVal,
|
305 | maxVal,
|
306 | minI,
|
307 | maxI,
|
308 | fillColor = series.fillOpacity ?
|
309 | new Color(series.color).setOpacity(pick(options.fillOpacity, 0.75)).get() :
|
310 | series.color,
|
311 | stroke = function() {
|
312 | if (doFill) {
|
313 | ctx.fillStyle = fillColor;
|
314 | ctx.fill();
|
315 | } else {
|
316 | ctx.strokeStyle = series.color;
|
317 | ctx.lineWidth = options.lineWidth;
|
318 | ctx.stroke();
|
319 | }
|
320 | },
|
321 | drawPoint = function(clientX, plotY, yBottom) {
|
322 | if (c === 0) {
|
323 | ctx.beginPath();
|
324 |
|
325 | if (cvsLineTo) {
|
326 | ctx.lineJoin = 'round';
|
327 | }
|
328 | }
|
329 |
|
330 | if (wasNull) {
|
331 | ctx.moveTo(clientX, plotY);
|
332 | } else {
|
333 | if (cvsDrawPoint) {
|
334 | cvsDrawPoint(ctx, clientX, plotY, yBottom, lastPoint);
|
335 | } else if (cvsLineTo) {
|
336 | cvsLineTo(ctx, clientX, plotY);
|
337 | } else if (cvsMarker) {
|
338 | cvsMarker(ctx, clientX, plotY, r);
|
339 | }
|
340 | }
|
341 |
|
342 | // We need to stroke the line for every 1000 pixels. It will crash the browser
|
343 | // memory use if we stroke too infrequently.
|
344 | c = c + 1;
|
345 | if (c === 1000) {
|
346 | stroke();
|
347 | c = 0;
|
348 | }
|
349 |
|
350 | // Area charts need to keep track of the last point
|
351 | lastPoint = {
|
352 | clientX: clientX,
|
353 | plotY: plotY,
|
354 | yBottom: yBottom
|
355 | };
|
356 | },
|
357 |
|
358 | addKDPoint = function(clientX, plotY, i) {
|
359 |
|
360 | // The k-d tree requires series points. Reduce the amount of points, since the time to build the
|
361 | // tree increases exponentially.
|
362 | if (enableMouseTracking && !pointTaken[clientX + ',' + plotY]) {
|
363 | pointTaken[clientX + ',' + plotY] = true;
|
364 |
|
365 | if (chart.inverted) {
|
366 | clientX = xAxis.len - clientX;
|
367 | plotY = yAxis.len - plotY;
|
368 | }
|
369 |
|
370 | points.push({
|
371 | clientX: clientX,
|
372 | plotX: clientX,
|
373 | plotY: plotY,
|
374 | i: cropStart + i
|
375 | });
|
376 | }
|
377 | };
|
378 |
|
379 | // If we are zooming out from SVG mode, destroy the graphics
|
380 | if (this.points || this.graph) {
|
381 | this.destroyGraphics();
|
382 | }
|
383 |
|
384 | // The group
|
385 | series.plotGroup(
|
386 | 'group',
|
387 | 'series',
|
388 | series.visible ? 'visible' : 'hidden',
|
389 | options.zIndex,
|
390 | chart.seriesGroup
|
391 | );
|
392 |
|
393 | series.markerGroup = series.group;
|
394 | addEvent(series, 'destroy', function() {
|
395 | series.markerGroup = null;
|
396 | });
|
397 |
|
398 | points = this.points = [];
|
399 | ctx = this.getContext();
|
400 | series.buildKDTree = noop; // Do not start building while drawing
|
401 |
|
402 | // Display a loading indicator
|
403 | if (rawData.length > 99999) {
|
404 | chart.options.loading = merge(loadingOptions, {
|
405 | labelStyle: {
|
406 | backgroundColor: H.color('#ffffff').setOpacity(0.75).get(),
|
407 | padding: '1em',
|
408 | borderRadius: '0.5em'
|
409 | },
|
410 | style: {
|
411 | backgroundColor: 'none',
|
412 | opacity: 1
|
413 | }
|
414 | });
|
415 | clearTimeout(destroyLoadingDiv);
|
416 | chart.showLoading('Drawing...');
|
417 | chart.options.loading = loadingOptions; // reset
|
418 | }
|
419 |
|
420 | // Loop over the points
|
421 | eachAsync(isStacked ? series.data : (xData || rawData), function(d, i) {
|
422 | var x,
|
423 | y,
|
424 | clientX,
|
425 | plotY,
|
426 | isNull,
|
427 | low,
|
428 | chartDestroyed = typeof chart.index === 'undefined',
|
429 | isYInside = true;
|
430 |
|
431 | if (!chartDestroyed) {
|
432 | if (useRaw) {
|
433 | x = d[0];
|
434 | y = d[1];
|
435 | } else {
|
436 | x = d;
|
437 | y = yData[i];
|
438 | }
|
439 |
|
440 | // Resolve low and high for range series
|
441 | if (isRange) {
|
442 | if (useRaw) {
|
443 | y = d.slice(1, 3);
|
444 | }
|
445 | low = y[0];
|
446 | y = y[1];
|
447 | } else if (isStacked) {
|
448 | x = d.x;
|
449 | y = d.stackY;
|
450 | low = y - d.y;
|
451 | }
|
452 |
|
453 | isNull = y === null;
|
454 |
|
455 | // Optimize for scatter zooming
|
456 | if (!requireSorting) {
|
457 | isYInside = y >= yMin && y <= yMax;
|
458 | }
|
459 |
|
460 | if (!isNull && x >= xMin && x <= xMax && isYInside) {
|
461 |
|
462 | clientX = Math.round(xAxis.toPixels(x, true));
|
463 |
|
464 | if (sampling) {
|
465 | if (minI === undefined || clientX === lastClientX) {
|
466 | if (!isRange) {
|
467 | low = y;
|
468 | }
|
469 | if (maxI === undefined || y > maxVal) {
|
470 | maxVal = y;
|
471 | maxI = i;
|
472 | }
|
473 | if (minI === undefined || low < minVal) {
|
474 | minVal = low;
|
475 | minI = i;
|
476 | }
|
477 |
|
478 | }
|
479 | if (clientX !== lastClientX) { // Add points and reset
|
480 | if (minI !== undefined) { // then maxI is also a number
|
481 | plotY = yAxis.toPixels(maxVal, true);
|
482 | yBottom = yAxis.toPixels(minVal, true);
|
483 | drawPoint(
|
484 | clientX,
|
485 | hasThreshold ? Math.min(plotY, translatedThreshold) : plotY,
|
486 | hasThreshold ? Math.max(yBottom, translatedThreshold) : yBottom
|
487 | );
|
488 | addKDPoint(clientX, plotY, maxI);
|
489 | if (yBottom !== plotY) {
|
490 | addKDPoint(clientX, yBottom, minI);
|
491 | }
|
492 | }
|
493 |
|
494 |
|
495 | minI = maxI = undefined;
|
496 | lastClientX = clientX;
|
497 | }
|
498 | } else {
|
499 | plotY = Math.round(yAxis.toPixels(y, true));
|
500 | drawPoint(clientX, plotY, yBottom);
|
501 | addKDPoint(clientX, plotY, i);
|
502 | }
|
503 | }
|
504 | wasNull = isNull && !connectNulls;
|
505 |
|
506 | if (i % CHUNK_SIZE === 0) {
|
507 | series.canvasToSVG();
|
508 | }
|
509 | }
|
510 |
|
511 | return !chartDestroyed;
|
512 | }, function() {
|
513 | var loadingDiv = chart.loadingDiv,
|
514 | loadingShown = chart.loadingShown;
|
515 | stroke();
|
516 | series.canvasToSVG();
|
517 |
|
518 | fireEvent(series, 'renderedCanvas');
|
519 |
|
520 | // Do not use chart.hideLoading, as it runs JS animation and will be blocked by buildKDTree.
|
521 | // CSS animation looks good, but then it must be deleted in timeout. If we add the module to core,
|
522 | // change hideLoading so we can skip this block.
|
523 | if (loadingShown) {
|
524 | extend(loadingDiv.style, {
|
525 | transition: 'opacity 250ms',
|
526 | opacity: 0
|
527 | });
|
528 | chart.loadingShown = false;
|
529 | destroyLoadingDiv = setTimeout(function() {
|
530 | if (loadingDiv.parentNode) { // In exporting it is falsy
|
531 | loadingDiv.parentNode.removeChild(loadingDiv);
|
532 | }
|
533 | chart.loadingDiv = chart.loadingSpan = null;
|
534 | }, 250);
|
535 | }
|
536 |
|
537 | // Pass tests in Pointer.
|
538 | // Replace this with a single property, and replace when zooming in
|
539 | // below boostThreshold.
|
540 | series.directTouch = false;
|
541 | series.options.stickyTracking = true;
|
542 |
|
543 | delete series.buildKDTree; // Go back to prototype, ready to build
|
544 | series.buildKDTree();
|
545 |
|
546 | // Don't do async on export, the exportChart, getSVGForExport and getSVG methods are not chained for it.
|
547 | }, chart.renderer.forExport ? Number.MAX_VALUE : undefined);
|
548 | }
|
549 | });
|
550 |
|
551 | seriesTypes.scatter.prototype.cvsMarkerCircle = function(ctx, clientX, plotY, r) {
|
552 | ctx.moveTo(clientX, plotY);
|
553 | ctx.arc(clientX, plotY, r, 0, 2 * Math.PI, false);
|
554 | };
|
555 |
|
556 | // Rect is twice as fast as arc, should be used for small markers
|
557 | seriesTypes.scatter.prototype.cvsMarkerSquare = function(ctx, clientX, plotY, r) {
|
558 | ctx.rect(clientX - r, plotY - r, r * 2, r * 2);
|
559 | };
|
560 | seriesTypes.scatter.prototype.fill = true;
|
561 |
|
562 | extend(seriesTypes.area.prototype, {
|
563 | cvsDrawPoint: function(ctx, clientX, plotY, yBottom, lastPoint) {
|
564 | if (lastPoint && clientX !== lastPoint.clientX) {
|
565 | ctx.moveTo(lastPoint.clientX, lastPoint.yBottom);
|
566 | ctx.lineTo(lastPoint.clientX, lastPoint.plotY);
|
567 | ctx.lineTo(clientX, plotY);
|
568 | ctx.lineTo(clientX, yBottom);
|
569 | }
|
570 | },
|
571 | fill: true,
|
572 | fillOpacity: true,
|
573 | sampling: true
|
574 | });
|
575 |
|
576 | extend(seriesTypes.column.prototype, {
|
577 | cvsDrawPoint: function(ctx, clientX, plotY, yBottom) {
|
578 | ctx.rect(clientX - 1, plotY, 1, yBottom - plotY);
|
579 | },
|
580 | fill: true,
|
581 | sampling: true
|
582 | });
|
583 |
|
584 | /**
|
585 | * Return a full Point object based on the index. The boost module uses stripped point objects
|
586 | * for performance reasons.
|
587 | * @param {Number} boostPoint A stripped-down point object
|
588 | * @returns {Object} A Point object as per http://api.highcharts.com/highcharts#Point
|
589 | */
|
590 | Series.prototype.getPoint = function(boostPoint) {
|
591 | var point = boostPoint;
|
592 |
|
593 | if (boostPoint && !(boostPoint instanceof this.pointClass)) {
|
594 | point = (new this.pointClass()).init(this, this.options.data[boostPoint.i]); // eslint-disable-line new-cap
|
595 | point.category = point.x;
|
596 |
|
597 | point.dist = boostPoint.dist;
|
598 | point.distX = boostPoint.distX;
|
599 | point.plotX = boostPoint.plotX;
|
600 | point.plotY = boostPoint.plotY;
|
601 | }
|
602 |
|
603 | return point;
|
604 | };
|
605 |
|
606 | /**
|
607 | * Extend series.destroy to also remove the fake k-d-tree points (#5137). Normally
|
608 | * this is handled by Series.destroy that calls Point.destroy, but the fake
|
609 | * search points are not registered like that.
|
610 | */
|
611 | wrap(Series.prototype, 'destroy', function(proceed) {
|
612 | var series = this,
|
613 | chart = series.chart;
|
614 | if (chart.hoverPoints) {
|
615 | chart.hoverPoints = grep(chart.hoverPoints, function(point) {
|
616 | return point.series === series;
|
617 | });
|
618 | }
|
619 |
|
620 | if (chart.hoverPoint && chart.hoverPoint.series === series) {
|
621 | chart.hoverPoint = null;
|
622 | }
|
623 | proceed.call(this);
|
624 | });
|
625 |
|
626 | /**
|
627 | * Return a point instance from the k-d-tree
|
628 | */
|
629 | wrap(Series.prototype, 'searchPoint', function(proceed) {
|
630 | return this.getPoint(
|
631 | proceed.apply(this, [].slice.call(arguments, 1))
|
632 | );
|
633 | });
|
634 |
|
635 | }(Highcharts));
|
636 | }));
|