StockChart.js 29 KB

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