timeline.src.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. /* *
  2. *
  3. * Experimental Timeline Series.
  4. * Note: This API is in alpha stage and will be changed before final release.
  5. *
  6. * (c) 2010-2019 Highsoft AS
  7. *
  8. * Author: Daniel Studencki
  9. *
  10. * License: www.highcharts.com/license
  11. *
  12. * */
  13. 'use strict';
  14. import H from '../parts/Globals.js';
  15. var addEvent = H.addEvent,
  16. extend = H.extend,
  17. defined = H.defined,
  18. LegendSymbolMixin = H.LegendSymbolMixin,
  19. TrackerMixin = H.TrackerMixin,
  20. merge = H.merge,
  21. pick = H.pick,
  22. Point = H.Point,
  23. Series = H.Series,
  24. undocumentedSeriesType = H.seriesType;
  25. /* *
  26. * The timeline series type.
  27. *
  28. * @private
  29. * @class
  30. * @name Highcharts.seriesTypes.timeline
  31. *
  32. * @augments Highcharts.Series
  33. */
  34. undocumentedSeriesType('timeline', 'line'
  35. /* *
  36. * The timeline series presents given events along a drawn line.
  37. *
  38. * @sample highcharts/series-timeline/alternate-labels Timeline series
  39. *
  40. * @extends plotOptions.line
  41. * @since 7.0.0
  42. * @product highcharts
  43. * @excluding animationLimit, boostThreshold, connectEnds, connectNulls,
  44. * cropThreshold, dashStyle, findNearestPointBy,
  45. * getExtremesFromAll, lineWidth, negativeColor, pointInterval,
  46. * pointIntervalUnit, pointPlacement, pointStart, softThreshold,
  47. * stacking, step, threshold, turboThreshold, zoneAxis, zones
  48. * @optionparent plotOptions.timeline
  49. */
  50. , {
  51. colorByPoint: true,
  52. stickyTracking: false,
  53. ignoreHiddenPoint: true,
  54. legendType: 'point',
  55. lineWidth: 0,
  56. tooltip: {
  57. headerFormat: '<span style="color:{point.color}">● </span>' +
  58. '<span style="font-weight: bold;">{point.point.date}</span><br/>',
  59. pointFormat: '{point.description}'
  60. },
  61. states: {
  62. hover: {
  63. lineWidthPlus: 5,
  64. halo: {
  65. size: 0
  66. }
  67. }
  68. },
  69. dataLabels: {
  70. enabled: true,
  71. allowOverlap: true,
  72. /* *
  73. * The width of the line connecting the data label to the point.
  74. *
  75. *
  76. * In styled mode, the connector stroke width is given in the
  77. * `.highcharts-data-label-connector` class.
  78. *
  79. * @type {Number}
  80. * @default 1
  81. * @sample {highcharts} highcharts/series-timeline/connector-styles
  82. * Custom connector width and color
  83. */
  84. connectorWidth: 1,
  85. /* *
  86. * The color of the line connecting the data label to the point.
  87. *
  88. * In styled mode, the connector stroke is given in the
  89. * `.highcharts-data-label-connector` class.
  90. *
  91. * @type {String}
  92. * @sample {highcharts} highcharts/series-timeline/connector-styles
  93. * Custom connector width and color
  94. */
  95. connectorColor: '#000000',
  96. backgroundColor: '#ffffff',
  97. /* *
  98. * @type {Highcharts.FormatterCallbackFunction<object>}
  99. * @default function () {
  100. * var format;
  101. *
  102. * if (!this.series.chart.styledMode) {
  103. * format = '<span style="color:' + this.point.color +
  104. * '">● </span><span style="font-weight: bold;" > ' +
  105. * (this.point.date || '') + '</span><br/>' +
  106. * (this.point.label || '');
  107. * } else {
  108. * format = '<span>● </span>' +
  109. * '<span>' + (this.point.date || '') +
  110. * '</span><br/>' + (this.point.label || '');
  111. * }
  112. * return format;
  113. * }
  114. * @apioption plotOptions.timeline.dataLabels.formatter
  115. */
  116. formatter: function () {
  117. var format;
  118. if (!this.series.chart.styledMode) {
  119. format = '<span style="color:' + this.point.color +
  120. '">● </span><span style="font-weight: bold;" > ' +
  121. (this.point.date || '') + '</span><br/>' +
  122. (this.point.label || '');
  123. } else {
  124. format = '<span>● </span>' +
  125. '<span>' + (this.point.date || '') +
  126. '</span><br/>' + (this.point.label || '');
  127. }
  128. return format;
  129. },
  130. borderWidth: 1,
  131. borderColor: '#666666',
  132. /* *
  133. * A pixel value defining the distance between the data label
  134. * and the point. Negative numbers puts the label on top
  135. * of the point.
  136. *
  137. * @type {Number}
  138. * @default 100
  139. */
  140. distance: 100,
  141. /* *
  142. * Whether to position data labels alternately. For example, if
  143. * [distance](#plotOptions.timeline.dataLabels.distance) is set
  144. * equal to `100`, then the first data label 's distance will be
  145. * set equal to `100`, the second one equal to `-100`, and so on.
  146. *
  147. * @type {Boolean}
  148. * @default true
  149. * @sample {highcharts} highcharts/series-timeline/alternate-disabled
  150. * Alternate disabled
  151. */
  152. alternate: true,
  153. verticalAlign: 'middle',
  154. color: '#333333'
  155. },
  156. marker: {
  157. enabledThreshold: 0,
  158. symbol: 'square',
  159. height: 15
  160. }
  161. }
  162. /* *
  163. * @lends Highcharts.Series#
  164. */
  165. , {
  166. requireSorting: false,
  167. trackerGroups: ['markerGroup', 'dataLabelsGroup'],
  168. // Use a simple symbol from LegendSymbolMixin
  169. drawLegendSymbol: LegendSymbolMixin.drawRectangle,
  170. // Use a group of trackers from TrackerMixin
  171. drawTracker: TrackerMixin.drawTrackerPoint,
  172. init: function () {
  173. var series = this;
  174. Series.prototype.init.apply(series, arguments);
  175. // Distribute data labels before rendering them. Distribution is
  176. // based on the 'dataLabels.distance' and 'dataLabels.alternate'
  177. // property.
  178. addEvent(series, 'drawDataLabels', function () {
  179. // Delete the oldTextWidth parameter, in order to force
  180. // adjusting data label wrapper box width. It's needed only when
  181. // useHTML is enabled. This prevents the data label text getting
  182. // out of the box range.
  183. if (series.options.dataLabels.useHTML) {
  184. series.points.forEach(function (p) {
  185. if (p.visible && p.dataLabel) {
  186. delete p.dataLabel.text.oldTextWidth;
  187. }
  188. });
  189. }
  190. // Distribute data labels basing on defined algorithm.
  191. series.distributeDL();
  192. });
  193. addEvent(series, 'afterDrawDataLabels', function () {
  194. var seriesOptions = series.options,
  195. options = seriesOptions.dataLabels,
  196. hasRendered = series.hasRendered || 0,
  197. defer = pick(options.defer, !!seriesOptions.animation),
  198. connectorsGroup = series.connectorsGroup,
  199. dataLabel;
  200. // Create (or redraw) the group for all connectors.
  201. connectorsGroup = series.plotGroup(
  202. 'connectorsGroup',
  203. 'data-labels-connectors',
  204. defer && !hasRendered ? 'hidden' : 'visible',
  205. options.zIndex || 5
  206. );
  207. // Draw or align connector for each point.
  208. series.points.forEach(function (point) {
  209. dataLabel = point.dataLabel;
  210. if (dataLabel) {
  211. // Within this wrap method is necessary to save the
  212. // current animation params, because the data label
  213. // target position (after animation) is needed to align
  214. // connectors.
  215. dataLabel.animate = function (params) {
  216. if (this.targetPosition) {
  217. this.targetPosition = params;
  218. }
  219. return H.SVGElement.prototype.animate.apply(
  220. this,
  221. arguments
  222. );
  223. };
  224. // Initialize the targetPosition field within data label
  225. // object. It's necessary because there is need to know
  226. // expected position of specific data label, when
  227. // aligning connectors. This field is overrided inside
  228. // of SVGElement.animate() wrapped method.
  229. if (!dataLabel.targetPosition) {
  230. dataLabel.targetPosition = {};
  231. }
  232. return !point.connector ?
  233. point.drawConnector() :
  234. point.alignConnector();
  235. }
  236. });
  237. // Animate connectors group. It's animated in the same way like
  238. // dataLabels, and also depends on dataLabels.defer parameter.
  239. if (defer) {
  240. connectorsGroup.attr({
  241. opacity: +hasRendered
  242. });
  243. if (!hasRendered) {
  244. addEvent(series, 'afterAnimate', function () {
  245. if (series.visible) {
  246. connectorsGroup.show(true);
  247. }
  248. connectorsGroup[
  249. seriesOptions.animation ? 'animate' : 'attr'
  250. ]({
  251. opacity: 1
  252. }, {
  253. duration: 200
  254. });
  255. });
  256. }
  257. }
  258. });
  259. },
  260. alignDataLabel: function (point, dataLabel) {
  261. var series = this,
  262. isInverted = series.chart.inverted,
  263. visiblePoints = series.visibilityMap.filter(function (point) {
  264. return point;
  265. }),
  266. visiblePointsCount = series.visiblePointsCount,
  267. pointIndex = visiblePoints.indexOf(point),
  268. isFirstOrLast = !pointIndex ||
  269. pointIndex === visiblePointsCount - 1,
  270. dataLabelsOptions = series.options.dataLabels,
  271. userDLOptions = point.userDLOptions || {},
  272. // Define multiplier which is used to calculate data label
  273. // width. If data labels are alternate, they have two times more
  274. // space to adapt (excepting first and last ones, which has only
  275. // one and half), than in case of placing all data labels side
  276. // by side.
  277. multiplier = dataLabelsOptions.alternate ?
  278. (isFirstOrLast ? 1.5 : 2) :
  279. 1,
  280. distance,
  281. availableSpace = Math.floor(
  282. series.xAxis.len / visiblePointsCount
  283. ),
  284. pad = dataLabel.padding,
  285. targetDLWidth,
  286. styles;
  287. // Adjust data label width to the currently available space.
  288. if (point.visible) {
  289. distance = Math.abs(
  290. userDLOptions.x || point.options.dataLabels.x
  291. );
  292. if (isInverted) {
  293. targetDLWidth = (
  294. (distance - pad) * 2 - (point.itemHeight / 2)
  295. );
  296. styles = {
  297. width: targetDLWidth,
  298. // Apply ellipsis when data label height is exceeded.
  299. textOverflow: dataLabel.width / targetDLWidth *
  300. dataLabel.height / 2 > availableSpace * multiplier ?
  301. 'ellipsis' : 'none'
  302. };
  303. } else {
  304. styles = {
  305. width: userDLOptions.width ||
  306. dataLabelsOptions.width ||
  307. availableSpace * multiplier - (pad * 2)
  308. };
  309. }
  310. dataLabel.css(styles);
  311. if (!series.chart.styledMode) {
  312. dataLabel.shadow({});
  313. }
  314. }
  315. Series.prototype.alignDataLabel.apply(series, arguments);
  316. },
  317. processData: function () {
  318. var series = this,
  319. xMap = [],
  320. base,
  321. visiblePoints = 0,
  322. i;
  323. series.visibilityMap = series.getVisibilityMap();
  324. // Calculate currently visible points.
  325. series.visibilityMap.forEach(function (point) {
  326. if (point) {
  327. visiblePoints++;
  328. }
  329. });
  330. series.visiblePointsCount = visiblePoints;
  331. base = series.xAxis.options.max / visiblePoints;
  332. // Generate xData map.
  333. for (i = 1; i <= visiblePoints; i++) {
  334. xMap.push(
  335. (base * i) - (base / 2)
  336. );
  337. }
  338. // Set all hidden points y values as negatives, in order to move
  339. // them away from plot area. It is necessary to avoid hiding data
  340. // labels, when dataLabels.allowOverlap is set to false.
  341. series.visibilityMap.forEach(function (vis, i) {
  342. if (!vis) {
  343. xMap.splice(i, 0, series.yData[i] === null ? null : -99);
  344. }
  345. });
  346. series.xData = xMap;
  347. series.yData = xMap.map(function (data) {
  348. return defined(data) ? 1 : null;
  349. });
  350. Series.prototype.processData.call(this, arguments);
  351. },
  352. generatePoints: function () {
  353. var series = this;
  354. Series.prototype.generatePoints.apply(series);
  355. series.points.forEach(function (point, i) {
  356. point.applyOptions({
  357. x: series.xData[i]
  358. });
  359. });
  360. },
  361. getVisibilityMap: function () {
  362. var series = this,
  363. map = (series.data.length ?
  364. series.data : series.userOptions.data
  365. ).map(function (point) {
  366. return (
  367. point &&
  368. point.visible !== false &&
  369. !point.isNull
  370. ) ? point : false;
  371. });
  372. return map;
  373. },
  374. distributeDL: function () {
  375. var series = this,
  376. dataLabelsOptions = series.options.dataLabels,
  377. options,
  378. pointDLOptions,
  379. newOptions = {},
  380. visibilityIndex = 1,
  381. distance = dataLabelsOptions.distance;
  382. series.points.forEach(function (point) {
  383. if (point.visible && !point.isNull) {
  384. options = point.options;
  385. pointDLOptions = point.options.dataLabels;
  386. if (!series.hasRendered) {
  387. point.userDLOptions = merge({}, pointDLOptions);
  388. }
  389. newOptions[series.chart.inverted ? 'x' : 'y'] =
  390. dataLabelsOptions.alternate && visibilityIndex % 2 ?
  391. -distance : distance;
  392. options.dataLabels = merge(newOptions, point.userDLOptions);
  393. visibilityIndex++;
  394. }
  395. });
  396. },
  397. markerAttribs: function (point, state) {
  398. var series = this,
  399. seriesMarkerOptions = series.options.marker,
  400. seriesStateOptions,
  401. pointMarkerOptions = point.marker || {},
  402. symbol = (
  403. pointMarkerOptions.symbol || seriesMarkerOptions.symbol
  404. ),
  405. pointStateOptions,
  406. width = pick(
  407. pointMarkerOptions.width,
  408. seriesMarkerOptions.width,
  409. series.xAxis.len / series.visiblePointsCount
  410. ),
  411. height = pick(
  412. pointMarkerOptions.height,
  413. seriesMarkerOptions.height
  414. ),
  415. radius = 0,
  416. attribs;
  417. // Handle hover and select states
  418. if (state) {
  419. seriesStateOptions = seriesMarkerOptions.states[state] || {};
  420. pointStateOptions = pointMarkerOptions.states &&
  421. pointMarkerOptions.states[state] || {};
  422. radius = pick(
  423. pointStateOptions.radius,
  424. seriesStateOptions.radius,
  425. radius + (
  426. seriesStateOptions.radiusPlus ||
  427. 0
  428. )
  429. );
  430. }
  431. point.hasImage = symbol && symbol.indexOf('url') === 0;
  432. attribs = {
  433. x: Math.floor(point.plotX) - (width / 2) - (radius / 2),
  434. y: point.plotY - (height / 2) - (radius / 2),
  435. width: width + radius,
  436. height: height + radius
  437. };
  438. return attribs;
  439. },
  440. bindAxes: function () {
  441. var series = this,
  442. timelineXAxis = {
  443. gridLineWidth: 0,
  444. lineWidth: 0,
  445. min: 0,
  446. dataMin: 0,
  447. minPadding: 0,
  448. max: 100,
  449. dataMax: 100,
  450. maxPadding: 0,
  451. title: null,
  452. tickPositions: []
  453. },
  454. timelineYAxis = {
  455. gridLineWidth: 0,
  456. min: 0.5,
  457. dataMin: 0.5,
  458. minPadding: 0,
  459. max: 1.5,
  460. dataMax: 1.5,
  461. maxPadding: 0,
  462. title: null,
  463. labels: {
  464. enabled: false
  465. }
  466. };
  467. Series.prototype.bindAxes.call(series);
  468. extend(series.xAxis.options, timelineXAxis);
  469. extend(series.yAxis.options, timelineYAxis);
  470. }
  471. }
  472. /* *
  473. * @lends Highcharts.Point#
  474. */
  475. , {
  476. init: function () {
  477. var point = Point.prototype.init.apply(this, arguments);
  478. point.name = pick(point.name, point.date, 'Event');
  479. point.y = 1;
  480. return point;
  481. },
  482. // The setVisible method is taken from Pie series prototype, in order to
  483. // prevent importing whole Pie series.
  484. setVisible: function (vis, redraw) {
  485. var point = this,
  486. series = point.series,
  487. chart = series.chart,
  488. ignoreHiddenPoint = series.options.ignoreHiddenPoint;
  489. redraw = pick(redraw, ignoreHiddenPoint);
  490. if (vis !== point.visible) {
  491. // If called without an argument, toggle visibility
  492. point.visible = point.options.visible = vis =
  493. vis === undefined ? !point.visible : vis;
  494. // update userOptions.data
  495. series.options.data[series.data.indexOf(point)] = point.options;
  496. // Show and hide associated elements. This is performed
  497. // regardless of redraw or not, because chart.redraw only
  498. // handles full series.
  499. ['graphic', 'dataLabel', 'connector'].forEach(
  500. function (key) {
  501. if (point[key]) {
  502. point[key][vis ? 'show' : 'hide'](true);
  503. }
  504. }
  505. );
  506. if (point.legendItem) {
  507. chart.legend.colorizeItem(point, vis);
  508. }
  509. // #4170, hide halo after hiding point
  510. if (!vis && point.state === 'hover') {
  511. point.setState('');
  512. }
  513. // Handle ignore hidden slices
  514. if (ignoreHiddenPoint) {
  515. series.isDirty = true;
  516. }
  517. if (redraw) {
  518. chart.redraw();
  519. }
  520. }
  521. },
  522. setState: function () {
  523. var proceed = Series.prototype.pointClass.prototype.setState;
  524. // Prevent triggering the setState method on null points.
  525. if (!this.isNull) {
  526. proceed.apply(this, arguments);
  527. }
  528. },
  529. getConnectorPath: function () {
  530. var point = this,
  531. chart = point.series.chart,
  532. xAxisLen = point.series.xAxis.len,
  533. inverted = chart.inverted,
  534. direction = inverted ? 'x2' : 'y2',
  535. dl = point.dataLabel,
  536. targetDLPos = dl.targetPosition,
  537. coords = {
  538. x1: point.plotX,
  539. y1: point.plotY,
  540. x2: point.plotX,
  541. y2: targetDLPos.y || dl.y
  542. },
  543. negativeDistance = (
  544. coords[direction] < point.series.yAxis.len / 2
  545. ),
  546. path;
  547. // Recalculate coords when the chart is inverted.
  548. if (inverted) {
  549. coords = {
  550. x1: point.plotY,
  551. y1: xAxisLen - point.plotX,
  552. x2: targetDLPos.x || dl.x,
  553. y2: xAxisLen - point.plotX
  554. };
  555. }
  556. // Subtract data label width or height from expected coordinate so
  557. // that the connector would start from the appropriate edge.
  558. if (negativeDistance) {
  559. coords[direction] += dl[inverted ? 'width' : 'height'];
  560. }
  561. path = chart.renderer.crispLine([
  562. 'M',
  563. coords.x1,
  564. coords.y1,
  565. 'L',
  566. coords.x2,
  567. coords.y2
  568. ], dl.options.connectorWidth || 1);
  569. return path;
  570. },
  571. drawConnector: function () {
  572. var point = this,
  573. series = point.series,
  574. dlOptions = point.dataLabel.options = merge(
  575. {}, series.options.dataLabels,
  576. point.options.dataLabels
  577. );
  578. point.connector = series.chart.renderer
  579. .path(point.getConnectorPath())
  580. .add(series.connectorsGroup);
  581. if (!series.chart.styledMode) {
  582. point.connector.attr({
  583. stroke: dlOptions.connectorColor,
  584. 'stroke-width': dlOptions.connectorWidth,
  585. opacity: point.dataLabel.opacity
  586. });
  587. }
  588. },
  589. alignConnector: function () {
  590. var point = this,
  591. connector = point.connector,
  592. bBox = connector.getBBox(),
  593. isVisible = bBox.y > 0;
  594. connector[isVisible ? 'animate' : 'attr']({
  595. d: point.getConnectorPath()
  596. });
  597. }
  598. });
  599. // Hide/show connector related with a specific data label, after overlapping
  600. // detected.
  601. addEvent(H.Chart, 'afterHideOverlappingLabels', function () {
  602. var series = this.series,
  603. dataLabel,
  604. connector;
  605. series.forEach(function (series) {
  606. if (series.points) {
  607. series.points.forEach(function (point) {
  608. dataLabel = point.dataLabel;
  609. connector = point.connector;
  610. if (
  611. dataLabel &&
  612. dataLabel.targetPosition &&
  613. connector
  614. ) {
  615. connector.attr({
  616. opacity: dataLabel.targetPosition.opacity ||
  617. dataLabel.newOpacity
  618. });
  619. }
  620. });
  621. }
  622. });
  623. });
  624. /* *
  625. * The `timeline` series. If the [type](#series.timeline.type) option is
  626. * not specified, it is inherited from [chart.type](#chart.type).
  627. *
  628. * @extends series,plotOptions.timeline
  629. * @excluding animationLimit, boostThreshold, connectEnds, connectNulls,
  630. * cropThreshold, dashStyle, dataParser, dataURL, findNearestPointBy,
  631. * getExtremesFromAll, lineWidth, negativeColor,
  632. * pointInterval, pointIntervalUnit, pointPlacement, pointStart,
  633. * softThreshold, stacking, stack, step, threshold, turboThreshold,
  634. * zoneAxis, zones
  635. * @product highcharts
  636. * @apioption series.timeline
  637. */
  638. /* *
  639. * An array of data points for the series. For the `timeline` series type,
  640. * points can be given with three general parameters, `date`, `label`,
  641. * and `description`:
  642. *
  643. * Example:
  644. *
  645. * ```js
  646. * series: [{
  647. * type: 'timeline',
  648. * data: [{
  649. * date: 'Jan 2018',
  650. * label: 'Some event label',
  651. * description: 'Description to show in tooltip'
  652. * }]
  653. * }]
  654. * ```
  655. *
  656. * @sample {highcharts} highcharts/series-timeline/alternate-labels
  657. * Alternate labels
  658. *
  659. * @type {Array<number|*>}
  660. * @extends series.line.data
  661. * @excluding marker, x, y
  662. * @product highcharts
  663. * @apioption series.timeline.data
  664. */
  665. /* *
  666. * The date of event.
  667. *
  668. * @type {string}
  669. * @product highcharts
  670. * @apioption series.timeline.data.date
  671. */
  672. /* *
  673. * The label of event.
  674. *
  675. * @type {string}
  676. * @product highcharts
  677. * @apioption series.timeline.data.label
  678. */
  679. /* *
  680. * The description of event. This description will be shown in tooltip.
  681. *
  682. * @type {string}
  683. * @product highcharts
  684. * @apioption series.timeline.data.description
  685. */