UNPKG

26.3 kBJavaScriptView Raw
1/**
2 * @license Highcharts JS v5.0.2 (2016-10-26)
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. Modify the heatmap-canvas demo so that it uses this module.
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 'use strict';
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}));