813afe239c1f4074120ddfb4bddc36f1d285aba8.svn-base 23 KB

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