StockChart.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. /* *
  2. *
  3. * (c) 2010-2019 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. import H from './Globals.js';
  12. import U from './Utilities.js';
  13. var arrayMax = U.arrayMax, arrayMin = U.arrayMin, clamp = U.clamp, defined = U.defined, extend = U.extend, isNumber = U.isNumber, isString = U.isString, pick = U.pick, splat = U.splat;
  14. import './Chart.js';
  15. import './Axis.js';
  16. import './Point.js';
  17. import './Pointer.js';
  18. import './Series.js';
  19. import './SvgRenderer.js';
  20. // Has a dependency on Navigator due to the use of
  21. // defaultOptions.navigator
  22. import './Navigator.js';
  23. // Has a dependency on Scrollbar due to the use of
  24. // defaultOptions.scrollbar
  25. import './Scrollbar.js';
  26. // Has a dependency on RangeSelector due to the use of
  27. // defaultOptions.rangeSelector
  28. import './RangeSelector.js';
  29. var addEvent = H.addEvent, Axis = H.Axis, Chart = H.Chart, format = H.format, merge = H.merge, Point = H.Point, Renderer = H.Renderer, Series = H.Series, SVGRenderer = H.SVGRenderer, VMLRenderer = H.VMLRenderer, seriesProto = Series.prototype, seriesInit = seriesProto.init, seriesProcessData = seriesProto.processData, pointTooltipFormatter = Point.prototype.tooltipFormatter;
  30. /**
  31. * Compare the values of the series against the first non-null, non-
  32. * zero value in the visible range. The y axis will show percentage
  33. * or absolute change depending on whether `compare` is set to `"percent"`
  34. * or `"value"`. When this is applied to multiple series, it allows
  35. * comparing the development of the series against each other. Adds
  36. * a `change` field to every point object.
  37. *
  38. * @see [compareBase](#plotOptions.series.compareBase)
  39. * @see [Axis.setCompare()](/class-reference/Highcharts.Axis#setCompare)
  40. *
  41. * @sample {highstock} stock/plotoptions/series-compare-percent/
  42. * Percent
  43. * @sample {highstock} stock/plotoptions/series-compare-value/
  44. * Value
  45. *
  46. * @type {string}
  47. * @since 1.0.1
  48. * @product highstock
  49. * @apioption plotOptions.series.compare
  50. */
  51. /**
  52. * Defines if comparison should start from the first point within the visible
  53. * range or should start from the first point **before** the range.
  54. *
  55. * In other words, this flag determines if first point within the visible range
  56. * will have 0% (`compareStart=true`) or should have been already calculated
  57. * according to the previous point (`compareStart=false`).
  58. *
  59. * @sample {highstock} stock/plotoptions/series-comparestart/
  60. * Calculate compare within visible range
  61. *
  62. * @type {boolean}
  63. * @default false
  64. * @since 6.0.0
  65. * @product highstock
  66. * @apioption plotOptions.series.compareStart
  67. */
  68. /**
  69. * When [compare](#plotOptions.series.compare) is `percent`, this option
  70. * dictates whether to use 0 or 100 as the base of comparison.
  71. *
  72. * @sample {highstock} stock/plotoptions/series-comparebase/
  73. * Compare base is 100
  74. *
  75. * @type {number}
  76. * @default 0
  77. * @since 5.0.6
  78. * @product highstock
  79. * @validvalue [0, 100]
  80. * @apioption plotOptions.series.compareBase
  81. */
  82. /* eslint-disable no-invalid-this, valid-jsdoc */
  83. /**
  84. * Factory function for creating new stock charts. Creates a new
  85. * {@link Highcharts.Chart|Chart} object with different default options than the
  86. * basic Chart.
  87. *
  88. * @example
  89. * var chart = Highcharts.stockChart('container', {
  90. * series: [{
  91. * data: [1, 2, 3, 4, 5, 6, 7, 8, 9],
  92. * pointInterval: 24 * 60 * 60 * 1000
  93. * }]
  94. * });
  95. *
  96. * @function Highcharts.stockChart
  97. *
  98. * @param {string|Highcharts.HTMLDOMElement} [renderTo]
  99. * The DOM element to render to, or its id.
  100. *
  101. * @param {Highcharts.Options} options
  102. * The chart options structure as described in the
  103. * [options reference](https://api.highcharts.com/highstock).
  104. *
  105. * @param {Highcharts.ChartCallbackFunction} [callback]
  106. * A function to execute when the chart object is finished loading and
  107. * rendering. In most cases the chart is built in one thread, but in
  108. * Internet Explorer version 8 or less the chart is sometimes
  109. * initialized before the document is ready, and in these cases the
  110. * chart object will not be finished synchronously. As a consequence,
  111. * code that relies on the newly built Chart object should always run in
  112. * the callback. Defining a
  113. * [chart.events.load](https://api.highcharts.com/highstock/chart.events.load)
  114. * handler is equivalent.
  115. *
  116. * @return {Highcharts.Chart}
  117. * The chart object.
  118. */
  119. H.StockChart = H.stockChart = function (a, b, c) {
  120. var hasRenderToArg = isString(a) || a.nodeName, options = arguments[hasRenderToArg ? 1 : 0], userOptions = options,
  121. // to increase performance, don't merge the data
  122. seriesOptions = options.series, defaultOptions = H.getOptions(), opposite, panning = options.chart && options.chart.panning,
  123. // Always disable startOnTick:true on the main axis when the navigator
  124. // is enabled (#1090)
  125. navigatorEnabled = pick(options.navigator && options.navigator.enabled, defaultOptions.navigator.enabled, true), verticalPanningEnabled = panning && /y/.test(panning.type), disableStartOnTick = {
  126. startOnTick: false,
  127. endOnTick: false
  128. };
  129. // apply X axis options to both single and multi y axes
  130. options.xAxis = splat(options.xAxis || {}).map(function (xAxisOptions, i) {
  131. return merge({
  132. minPadding: 0,
  133. maxPadding: 0,
  134. overscroll: 0,
  135. ordinal: true,
  136. title: {
  137. text: null
  138. },
  139. labels: {
  140. overflow: 'justify'
  141. },
  142. showLastLabel: true
  143. }, defaultOptions.xAxis, // #3802
  144. defaultOptions.xAxis && defaultOptions.xAxis[i], // #7690
  145. xAxisOptions, // user options
  146. {
  147. type: 'datetime',
  148. categories: null
  149. }, (navigatorEnabled ? disableStartOnTick : null));
  150. });
  151. // apply Y axis options to both single and multi y axes
  152. options.yAxis = splat(options.yAxis || {}).map(function (yAxisOptions, i) {
  153. opposite = pick(yAxisOptions.opposite, true);
  154. return merge({
  155. labels: {
  156. y: -2
  157. },
  158. opposite: opposite,
  159. /**
  160. * @default {highcharts} true
  161. * @default {highstock} false
  162. * @apioption yAxis.showLastLabel
  163. *
  164. * @private
  165. */
  166. showLastLabel: !!(
  167. // #6104, show last label by default for category axes
  168. yAxisOptions.categories ||
  169. yAxisOptions.type === 'category'),
  170. title: {
  171. text: null
  172. }
  173. }, defaultOptions.yAxis, // #3802
  174. defaultOptions.yAxis && defaultOptions.yAxis[i], // #7690
  175. yAxisOptions, // user options
  176. (verticalPanningEnabled ? disableStartOnTick : null));
  177. });
  178. options.series = null;
  179. options = merge({
  180. chart: {
  181. panning: {
  182. enabled: true,
  183. type: 'x'
  184. },
  185. pinchType: 'x'
  186. },
  187. navigator: {
  188. enabled: navigatorEnabled
  189. },
  190. scrollbar: {
  191. // #4988 - check if setOptions was called
  192. enabled: pick(defaultOptions.scrollbar.enabled, true)
  193. },
  194. rangeSelector: {
  195. // #4988 - check if setOptions was called
  196. enabled: pick(defaultOptions.rangeSelector.enabled, true)
  197. },
  198. title: {
  199. text: null
  200. },
  201. tooltip: {
  202. split: pick(defaultOptions.tooltip.split, true),
  203. crosshairs: true
  204. },
  205. legend: {
  206. enabled: false
  207. }
  208. }, options, // user's options
  209. {
  210. isStock: true // internal flag
  211. });
  212. options.series = userOptions.series = seriesOptions;
  213. return hasRenderToArg ?
  214. new Chart(a, options, c) :
  215. new Chart(options, b);
  216. };
  217. // Handle som Stock-specific series defaults, override the plotOptions before
  218. // series options are handled.
  219. addEvent(Series, 'setOptions', function (e) {
  220. var series = this, overrides;
  221. /**
  222. * @private
  223. */
  224. function is(type) {
  225. return H.seriesTypes[type] && series instanceof H.seriesTypes[type];
  226. }
  227. if (this.chart.options.isStock) {
  228. if (is('column') || is('columnrange')) {
  229. overrides = {
  230. borderWidth: 0,
  231. shadow: false
  232. };
  233. }
  234. else if (is('line') && !is('scatter') && !is('sma')) {
  235. overrides = {
  236. marker: {
  237. enabled: false,
  238. radius: 2
  239. }
  240. };
  241. }
  242. if (overrides) {
  243. e.plotOptions[this.type] = merge(e.plotOptions[this.type], overrides);
  244. }
  245. }
  246. });
  247. // Override the automatic label alignment so that the first Y axis' labels
  248. // are drawn on top of the grid line, and subsequent axes are drawn outside
  249. addEvent(Axis, 'autoLabelAlign', function (e) {
  250. var chart = this.chart, options = this.options, panes = chart._labelPanes = chart._labelPanes || {}, key, labelOptions = this.options.labels;
  251. if (this.chart.options.isStock && this.coll === 'yAxis') {
  252. key = options.top + ',' + options.height;
  253. // do it only for the first Y axis of each pane
  254. if (!panes[key] && labelOptions.enabled) {
  255. if (labelOptions.x === 15) { // default
  256. labelOptions.x = 0;
  257. }
  258. if (typeof labelOptions.align === 'undefined') {
  259. labelOptions.align = 'right';
  260. }
  261. panes[key] = this;
  262. e.align = 'right';
  263. e.preventDefault();
  264. }
  265. }
  266. });
  267. // Clear axis from label panes (#6071)
  268. addEvent(Axis, 'destroy', function () {
  269. var chart = this.chart, key = this.options && (this.options.top + ',' + this.options.height);
  270. if (key && chart._labelPanes && chart._labelPanes[key] === this) {
  271. delete chart._labelPanes[key];
  272. }
  273. });
  274. // Override getPlotLinePath to allow for multipane charts
  275. addEvent(Axis, 'getPlotLinePath', function (e) {
  276. var axis = this, series = (this.isLinked && !this.series ?
  277. this.linkedParent.series :
  278. this.series), chart = axis.chart, renderer = chart.renderer, axisLeft = axis.left, axisTop = axis.top, x1, y1, x2, y2, result = [], axes = [], // #3416 need a default array
  279. axes2, uniqueAxes, translatedValue = e.translatedValue, value = e.value, force = e.force, transVal;
  280. /**
  281. * Return the other axis based on either the axis option or on related
  282. * series.
  283. * @private
  284. */
  285. function getAxis(coll) {
  286. var otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis', opt = axis.options[otherColl];
  287. // Other axis indexed by number
  288. if (isNumber(opt)) {
  289. return [chart[otherColl][opt]];
  290. }
  291. // Other axis indexed by id (like navigator)
  292. if (isString(opt)) {
  293. return [chart.get(opt)];
  294. }
  295. // Auto detect based on existing series
  296. return series.map(function (s) {
  297. return s[otherColl];
  298. });
  299. }
  300. if ( // For stock chart, by default render paths across the panes
  301. // except the case when `acrossPanes` is disabled by user (#6644)
  302. (chart.options.isStock && e.acrossPanes !== false) &&
  303. // Ignore in case of colorAxis or zAxis. #3360, #3524, #6720
  304. axis.coll === 'xAxis' || axis.coll === 'yAxis') {
  305. e.preventDefault();
  306. // Get the related axes based on series
  307. axes = getAxis(axis.coll);
  308. // Get the related axes based options.*Axis setting #2810
  309. axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis);
  310. axes2.forEach(function (A) {
  311. if (defined(A.options.id) ?
  312. A.options.id.indexOf('navigator') === -1 :
  313. true) {
  314. var a = (A.isXAxis ? 'yAxis' : 'xAxis'), rax = (defined(A.options[a]) ?
  315. chart[a][A.options[a]] :
  316. chart[a][0]);
  317. if (axis === rax) {
  318. axes.push(A);
  319. }
  320. }
  321. });
  322. // Remove duplicates in the axes array. If there are no axes in the axes
  323. // array, we are adding an axis without data, so we need to populate
  324. // this with grid lines (#2796).
  325. uniqueAxes = axes.length ?
  326. [] :
  327. [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742
  328. axes.forEach(function (axis2) {
  329. if (uniqueAxes.indexOf(axis2) === -1 &&
  330. // Do not draw on axis which overlap completely. #5424
  331. !H.find(uniqueAxes, function (unique) {
  332. return unique.pos === axis2.pos && unique.len === axis2.len;
  333. })) {
  334. uniqueAxes.push(axis2);
  335. }
  336. });
  337. transVal = pick(translatedValue, axis.translate(value, null, null, e.old));
  338. if (isNumber(transVal)) {
  339. if (axis.horiz) {
  340. uniqueAxes.forEach(function (axis2) {
  341. var skip;
  342. y1 = axis2.pos;
  343. y2 = y1 + axis2.len;
  344. x1 = x2 = Math.round(transVal + axis.transB);
  345. // outside plot area
  346. if (force !== 'pass' &&
  347. (x1 < axisLeft || x1 > axisLeft + axis.width)) {
  348. if (force) {
  349. x1 = x2 = clamp(x1, axisLeft, axisLeft + axis.width);
  350. }
  351. else {
  352. skip = true;
  353. }
  354. }
  355. if (!skip) {
  356. result.push('M', x1, y1, 'L', x2, y2);
  357. }
  358. });
  359. }
  360. else {
  361. uniqueAxes.forEach(function (axis2) {
  362. var skip;
  363. x1 = axis2.pos;
  364. x2 = x1 + axis2.len;
  365. y1 = y2 = Math.round(axisTop + axis.height - transVal);
  366. // outside plot area
  367. if (force !== 'pass' &&
  368. (y1 < axisTop || y1 > axisTop + axis.height)) {
  369. if (force) {
  370. y1 = y2 = clamp(y1, axisTop, axisTop + axis.height);
  371. }
  372. else {
  373. skip = true;
  374. }
  375. }
  376. if (!skip) {
  377. result.push('M', x1, y1, 'L', x2, y2);
  378. }
  379. });
  380. }
  381. }
  382. e.path = result.length > 0 ?
  383. renderer.crispPolyLine(result, e.lineWidth || 1) :
  384. // #3557 getPlotLinePath in regular Highcharts also returns null
  385. null;
  386. }
  387. });
  388. /**
  389. * Function to crisp a line with multiple segments
  390. *
  391. * @private
  392. * @function Highcharts.SVGRenderer#crispPolyLine
  393. * @param {Highcharts.SVGPathArray} points
  394. * @param {number} width
  395. * @return {Highcharts.SVGPathArray}
  396. */
  397. SVGRenderer.prototype.crispPolyLine = function (points, width) {
  398. // points format: ['M', 0, 0, 'L', 100, 0]
  399. // normalize to a crisp line
  400. var i;
  401. for (i = 0; i < points.length; i = i + 6) {
  402. if (points[i + 1] === points[i + 4]) {
  403. // Substract due to #1129. Now bottom and left axis gridlines behave
  404. // the same.
  405. points[i + 1] = points[i + 4] =
  406. Math.round(points[i + 1]) - (width % 2 / 2);
  407. }
  408. if (points[i + 2] === points[i + 5]) {
  409. points[i + 2] = points[i + 5] =
  410. Math.round(points[i + 2]) + (width % 2 / 2);
  411. }
  412. }
  413. return points;
  414. };
  415. if (Renderer === VMLRenderer) {
  416. VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine;
  417. }
  418. // Wrapper to hide the label
  419. addEvent(Axis, 'afterHideCrosshair', function () {
  420. if (this.crossLabel) {
  421. this.crossLabel = this.crossLabel.hide();
  422. }
  423. });
  424. // Extend crosshairs to also draw the label
  425. addEvent(Axis, 'afterDrawCrosshair', function (event) {
  426. // Check if the label has to be drawn
  427. if (!defined(this.crosshair.label) ||
  428. !this.crosshair.label.enabled ||
  429. !this.cross) {
  430. return;
  431. }
  432. var chart = this.chart, options = this.options.crosshair.label, // the label's options
  433. horiz = this.horiz, // axis orientation
  434. opposite = this.opposite, // axis position
  435. left = this.left, // left position
  436. top = this.top, // top position
  437. crossLabel = this.crossLabel, // the svgElement
  438. posx, posy, crossBox, formatOption = options.format, formatFormat = '', limit, align, tickInside = this.options.tickPosition === 'inside', snap = this.crosshair.snap !== false, value, offset = 0,
  439. // Use last available event (#5287)
  440. e = event.e || (this.cross && this.cross.e), point = event.point, lin2log = this.lin2log, min, max;
  441. if (this.isLog) {
  442. min = lin2log(this.min);
  443. max = lin2log(this.max);
  444. }
  445. else {
  446. min = this.min;
  447. max = this.max;
  448. }
  449. align = (horiz ? 'center' : opposite ?
  450. (this.labelAlign === 'right' ? 'right' : 'left') :
  451. (this.labelAlign === 'left' ? 'left' : 'center'));
  452. // If the label does not exist yet, create it.
  453. if (!crossLabel) {
  454. crossLabel = this.crossLabel = chart.renderer
  455. .label(null, null, null, options.shape || 'callout')
  456. .addClass('highcharts-crosshair-label' + (this.series[0] &&
  457. ' highcharts-color-' + this.series[0].colorIndex))
  458. .attr({
  459. align: options.align || align,
  460. padding: pick(options.padding, 8),
  461. r: pick(options.borderRadius, 3),
  462. zIndex: 2
  463. })
  464. .add(this.labelGroup);
  465. // Presentational
  466. if (!chart.styledMode) {
  467. crossLabel
  468. .attr({
  469. fill: options.backgroundColor ||
  470. (this.series[0] && this.series[0].color) ||
  471. '#666666',
  472. stroke: options.borderColor || '',
  473. 'stroke-width': options.borderWidth || 0
  474. })
  475. .css(extend({
  476. color: '#ffffff',
  477. fontWeight: 'normal',
  478. fontSize: '11px',
  479. textAlign: 'center'
  480. }, options.style));
  481. }
  482. }
  483. if (horiz) {
  484. posx = snap ? point.plotX + left : e.chartX;
  485. posy = top + (opposite ? 0 : this.height);
  486. }
  487. else {
  488. posx = opposite ? this.width + left : 0;
  489. posy = snap ? point.plotY + top : e.chartY;
  490. }
  491. if (!formatOption && !options.formatter) {
  492. if (this.isDatetimeAxis) {
  493. formatFormat = '%b %d, %Y';
  494. }
  495. formatOption =
  496. '{value' + (formatFormat ? ':' + formatFormat : '') + '}';
  497. }
  498. // Show the label
  499. value = snap ?
  500. point[this.isXAxis ? 'x' : 'y'] :
  501. this.toValue(horiz ? e.chartX : e.chartY);
  502. crossLabel.attr({
  503. text: formatOption ?
  504. format(formatOption, { value: value }, chart) :
  505. options.formatter.call(this, value),
  506. x: posx,
  507. y: posy,
  508. // Crosshair should be rendered within Axis range (#7219)
  509. visibility: value < min || value > max ?
  510. 'hidden' :
  511. 'visible'
  512. });
  513. crossBox = crossLabel.getBBox();
  514. // now it is placed we can correct its position
  515. if (horiz) {
  516. if ((tickInside && !opposite) || (!tickInside && opposite)) {
  517. posy = crossLabel.y - crossBox.height;
  518. }
  519. }
  520. else {
  521. posy = crossLabel.y - (crossBox.height / 2);
  522. }
  523. // check the edges
  524. if (horiz) {
  525. limit = {
  526. left: left - crossBox.x,
  527. right: left + this.width - crossBox.x
  528. };
  529. }
  530. else {
  531. limit = {
  532. left: this.labelAlign === 'left' ? left : 0,
  533. right: this.labelAlign === 'right' ?
  534. left + this.width :
  535. chart.chartWidth
  536. };
  537. }
  538. // left edge
  539. if (crossLabel.translateX < limit.left) {
  540. offset = limit.left - crossLabel.translateX;
  541. }
  542. // right edge
  543. if (crossLabel.translateX + crossBox.width >= limit.right) {
  544. offset = -(crossLabel.translateX + crossBox.width - limit.right);
  545. }
  546. // show the crosslabel
  547. crossLabel.attr({
  548. x: posx + offset,
  549. y: posy,
  550. // First set x and y, then anchorX and anchorY, when box is actually
  551. // calculated, #5702
  552. anchorX: horiz ?
  553. posx :
  554. (this.opposite ? 0 : chart.chartWidth),
  555. anchorY: horiz ?
  556. (this.opposite ? chart.chartHeight : 0) :
  557. posy + crossBox.height / 2
  558. });
  559. });
  560. /* ************************************************************************** *
  561. * Start value compare logic *
  562. * ************************************************************************** */
  563. /**
  564. * Extend series.init by adding a method to modify the y value used for plotting
  565. * on the y axis. This method is called both from the axis when finding dataMin
  566. * and dataMax, and from the series.translate method.
  567. *
  568. * @ignore
  569. * @function Highcharts.Series#init
  570. */
  571. seriesProto.init = function () {
  572. // Call base method
  573. seriesInit.apply(this, arguments);
  574. // Set comparison mode
  575. this.setCompare(this.options.compare);
  576. };
  577. /**
  578. * Highstock only. Set the
  579. * [compare](https://api.highcharts.com/highstock/plotOptions.series.compare)
  580. * mode of the series after render time. In most cases it is more useful running
  581. * {@link Axis#setCompare} on the X axis to update all its series.
  582. *
  583. * @function Highcharts.Series#setCompare
  584. *
  585. * @param {string} [compare]
  586. * Can be one of `null` (default), `"percent"` or `"value"`.
  587. */
  588. seriesProto.setCompare = function (compare) {
  589. // Set or unset the modifyValue method
  590. this.modifyValue = (compare === 'value' || compare === 'percent') ?
  591. function (value, point) {
  592. var compareValue = this.compareValue;
  593. if (typeof value !== 'undefined' &&
  594. typeof compareValue !== 'undefined') { // #2601, #5814
  595. // Get the modified value
  596. if (compare === 'value') {
  597. value -= compareValue;
  598. // Compare percent
  599. }
  600. else {
  601. value = 100 * (value / compareValue) -
  602. (this.options.compareBase === 100 ? 0 : 100);
  603. }
  604. // record for tooltip etc.
  605. if (point) {
  606. point.change = value;
  607. }
  608. return value;
  609. }
  610. return 0;
  611. } :
  612. null;
  613. // Survive to export, #5485
  614. this.userOptions.compare = compare;
  615. // Mark dirty
  616. if (this.chart.hasRendered) {
  617. this.isDirty = true;
  618. }
  619. };
  620. /**
  621. * Extend series.processData by finding the first y value in the plot area,
  622. * used for comparing the following values
  623. *
  624. * @ignore
  625. * @function Highcharts.Series#processData
  626. */
  627. seriesProto.processData = function (force) {
  628. var series = this, i, keyIndex = -1, processedXData, processedYData, compareStart = series.options.compareStart === true ? 0 : 1, length, compareValue;
  629. // call base method
  630. seriesProcessData.apply(this, arguments);
  631. if (series.xAxis && series.processedYData) { // not pies
  632. // local variables
  633. processedXData = series.processedXData;
  634. processedYData = series.processedYData;
  635. length = processedYData.length;
  636. // For series with more than one value (range, OHLC etc), compare
  637. // against close or the pointValKey (#4922, #3112, #9854)
  638. if (series.pointArrayMap) {
  639. keyIndex = series.pointArrayMap.indexOf(series.options.pointValKey || series.pointValKey || 'y');
  640. }
  641. // find the first value for comparison
  642. for (i = 0; i < length - compareStart; i++) {
  643. compareValue = processedYData[i] && keyIndex > -1 ?
  644. processedYData[i][keyIndex] :
  645. processedYData[i];
  646. if (isNumber(compareValue) &&
  647. processedXData[i + compareStart] >=
  648. series.xAxis.min &&
  649. compareValue !== 0) {
  650. series.compareValue = compareValue;
  651. break;
  652. }
  653. }
  654. }
  655. return;
  656. };
  657. // Modify series extremes
  658. addEvent(Series, 'afterGetExtremes', function () {
  659. if (this.modifyValue) {
  660. var extremes = [
  661. this.modifyValue(this.dataMin),
  662. this.modifyValue(this.dataMax)
  663. ];
  664. this.dataMin = arrayMin(extremes);
  665. this.dataMax = arrayMax(extremes);
  666. }
  667. });
  668. /**
  669. * Highstock only. Set the compare mode on all series belonging to an Y axis
  670. * after render time.
  671. *
  672. * @see [series.plotOptions.compare](https://api.highcharts.com/highstock/series.plotOptions.compare)
  673. *
  674. * @sample stock/members/axis-setcompare/
  675. * Set compoare
  676. *
  677. * @function Highcharts.Axis#setCompare
  678. *
  679. * @param {string} [compare]
  680. * The compare mode. Can be one of `null` (default), `"value"` or
  681. * `"percent"`.
  682. *
  683. * @param {boolean} [redraw=true]
  684. * Whether to redraw the chart or to wait for a later call to
  685. * {@link Chart#redraw}.
  686. */
  687. Axis.prototype.setCompare = function (compare, redraw) {
  688. if (!this.isXAxis) {
  689. this.series.forEach(function (series) {
  690. series.setCompare(compare);
  691. });
  692. if (pick(redraw, true)) {
  693. this.chart.redraw();
  694. }
  695. }
  696. };
  697. /**
  698. * Extend the tooltip formatter by adding support for the point.change variable
  699. * as well as the changeDecimals option.
  700. *
  701. * @ignore
  702. * @function Highcharts.Point#tooltipFormatter
  703. *
  704. * @param {string} pointFormat
  705. */
  706. Point.prototype.tooltipFormatter = function (pointFormat) {
  707. var point = this;
  708. var numberFormatter = point.series.chart.numberFormatter;
  709. pointFormat = pointFormat.replace('{point.change}', (point.change > 0 ? '+' : '') + numberFormatter(point.change, pick(point.series.tooltipOptions.changeDecimals, 2)));
  710. return pointTooltipFormatter.apply(this, [pointFormat]);
  711. };
  712. /* ************************************************************************** *
  713. * End value compare logic *
  714. * ************************************************************************** */
  715. // Extend the Series prototype to create a separate series clip box. This is
  716. // related to using multiple panes, and a future pane logic should incorporate
  717. // this feature (#2754).
  718. addEvent(Series, 'render', function () {
  719. var chart = this.chart, clipHeight;
  720. // Only do this on not 3d (#2939, #5904) nor polar (#6057) charts, and only
  721. // if the series type handles clipping in the animate method (#2975).
  722. if (!(chart.is3d && chart.is3d()) &&
  723. !chart.polar &&
  724. this.xAxis &&
  725. !this.xAxis.isRadial // Gauge, #6192
  726. ) {
  727. clipHeight = this.yAxis.len;
  728. // Include xAxis line width (#8031) but only if the Y axis ends on the
  729. // edge of the X axis (#11005).
  730. if (this.xAxis.axisLine) {
  731. var dist = chart.plotTop + chart.plotHeight -
  732. this.yAxis.pos - this.yAxis.len, lineHeightCorrection = Math.floor(this.xAxis.axisLine.strokeWidth() / 2);
  733. if (dist >= 0) {
  734. clipHeight -= Math.max(lineHeightCorrection - dist, 0);
  735. }
  736. }
  737. // First render, initial clip box
  738. if (!this.clipBox && this.animate) {
  739. this.clipBox = merge(chart.clipBox);
  740. this.clipBox.width = this.xAxis.len;
  741. this.clipBox.height = clipHeight;
  742. // On redrawing, resizing etc, update the clip rectangle
  743. }
  744. else if (chart[this.sharedClipKey]) {
  745. // animate in case resize is done during initial animation
  746. chart[this.sharedClipKey].animate({
  747. width: this.xAxis.len,
  748. height: clipHeight
  749. });
  750. // also change markers clip animation for consistency
  751. // (marker clip rects should exist only on chart init)
  752. if (chart[this.sharedClipKey + 'm']) {
  753. chart[this.sharedClipKey + 'm'].animate({
  754. width: this.xAxis.len
  755. });
  756. }
  757. }
  758. }
  759. });
  760. addEvent(Chart, 'update', function (e) {
  761. var options = e.options;
  762. // Use case: enabling scrollbar from a disabled state.
  763. // Scrollbar needs to be initialized from a controller, Navigator in this
  764. // case (#6615)
  765. if ('scrollbar' in options && this.navigator) {
  766. merge(true, this.options.scrollbar, options.scrollbar);
  767. this.navigator.update({}, false);
  768. delete options.scrollbar;
  769. }
  770. });
  771. // Extend the Axis prototype to calculate start min/max values
  772. // (including min/maxPadding). This is related to using vertical panning
  773. // (#11315).
  774. addEvent(Axis, 'afterSetScale', function () {
  775. var axis = this, panning = axis.chart.options.chart &&
  776. axis.chart.options.chart.panning;
  777. if (panning &&
  778. (panning.type === 'y' ||
  779. panning.type === 'xy') &&
  780. !axis.isXAxis &&
  781. !defined(axis.panningState)) {
  782. var min = Number.MAX_VALUE, max = Number.MIN_VALUE;
  783. axis.series.forEach(function (series) {
  784. min = Math.min(H.arrayMin(series.yData), min) -
  785. (axis.min && axis.dataMin ? axis.dataMin - axis.min : 0);
  786. max = Math.max(H.arrayMax(series.yData), max) +
  787. (axis.max && axis.dataMax ? axis.max - axis.dataMax : 0);
  788. });
  789. axis.panningState = {
  790. startMin: min,
  791. startMax: max
  792. };
  793. }
  794. });