boost.src.js 26 KB

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