parallel-coordinates.src.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. /* *
  2. * Parallel coordinates module
  3. *
  4. * (c) 2010-2019 Pawel Fus
  5. *
  6. * License: www.highcharts.com/license
  7. */
  8. 'use strict';
  9. import H from '../parts/Globals.js';
  10. import '../parts/Axis.js';
  11. import '../parts/Chart.js';
  12. import '../parts/Series.js';
  13. // Extensions for parallel coordinates plot.
  14. var Axis = H.Axis,
  15. Chart = H.Chart,
  16. ChartProto = Chart.prototype,
  17. AxisProto = H.Axis.prototype;
  18. var addEvent = H.addEvent,
  19. pick = H.pick,
  20. wrap = H.wrap,
  21. merge = H.merge,
  22. erase = H.erase,
  23. splat = H.splat,
  24. extend = H.extend,
  25. defined = H.defined,
  26. arrayMin = H.arrayMin,
  27. arrayMax = H.arrayMax;
  28. var defaultXAxisOptions = {
  29. lineWidth: 0,
  30. tickLength: 0,
  31. opposite: true,
  32. type: 'category'
  33. };
  34. /**
  35. * @optionparent chart
  36. */
  37. var defaultParallelOptions = {
  38. /**
  39. * Flag to render charts as a parallel coordinates plot. In a parallel
  40. * coordinates plot (||-coords) by default all required yAxes are generated
  41. * and the legend is disabled. This feature requires
  42. * `modules/parallel-coordinates.js`.
  43. *
  44. * @sample {highcharts} /highcharts/demo/parallel-coordinates/
  45. * Parallel coordinates demo
  46. * @sample {highcharts} highcharts/parallel-coordinates/polar/
  47. * Star plot, multivariate data in a polar chart
  48. *
  49. * @since 6.0.0
  50. * @product highcharts
  51. */
  52. parallelCoordinates: false,
  53. /**
  54. * Common options for all yAxes rendered in a parallel coordinates plot.
  55. * This feature requires `modules/parallel-coordinates.js`.
  56. *
  57. * The default options are:
  58. * <pre>
  59. * parallelAxes: {
  60. * lineWidth: 1, // classic mode only
  61. * gridlinesWidth: 0, // classic mode only
  62. * title: {
  63. * text: '',
  64. * reserveSpace: false
  65. * },
  66. * labels: {
  67. * x: 0,
  68. * y: 0,
  69. * align: 'center',
  70. * reserveSpace: false
  71. * },
  72. * offset: 0
  73. * }</pre>
  74. *
  75. * @sample {highcharts} highcharts/parallel-coordinates/parallelaxes/
  76. * Set the same tickAmount for all yAxes
  77. *
  78. * @extends yAxis
  79. * @since 6.0.0
  80. * @product highcharts
  81. * @excluding alternateGridColor, breaks, id, gridLineColor,
  82. * gridLineDashStyle, gridLineWidth, minorGridLineColor,
  83. * minorGridLineDashStyle, minorGridLineWidth, plotBands,
  84. * plotLines, angle, gridLineInterpolation, maxColor, maxZoom,
  85. * minColor, scrollbar, stackLabels, stops
  86. */
  87. parallelAxes: {
  88. lineWidth: 1,
  89. /**
  90. * Titles for yAxes are taken from
  91. * [xAxis.categories](#xAxis.categories). All options for `xAxis.labels`
  92. * applies to parallel coordinates titles. For example, to style
  93. * categories, use [xAxis.labels.style](#xAxis.labels.style).
  94. *
  95. * @excluding align, enabled, margin, offset, position3d, reserveSpace,
  96. * rotation, skew3d, style, text, useHTML, x, y
  97. */
  98. title: {
  99. text: '',
  100. reserveSpace: false
  101. },
  102. labels: {
  103. x: 0,
  104. y: 4,
  105. align: 'center',
  106. reserveSpace: false
  107. },
  108. offset: 0
  109. }
  110. };
  111. H.setOptions({
  112. chart: defaultParallelOptions
  113. });
  114. // Initialize parallelCoordinates
  115. addEvent(Chart, 'init', function (e) {
  116. var options = e.args[0],
  117. defaultyAxis = splat(options.yAxis || {}),
  118. yAxisLength = defaultyAxis.length,
  119. newYAxes = [];
  120. /**
  121. * Flag used in parallel coordinates plot to check if chart has ||-coords
  122. * (parallel coords).
  123. *
  124. * @requires module:modules/parallel-coordinates
  125. *
  126. * @name Highcharts.Chart#hasParallelCoordinates
  127. * @type {boolean}
  128. */
  129. this.hasParallelCoordinates = options.chart &&
  130. options.chart.parallelCoordinates;
  131. if (this.hasParallelCoordinates) {
  132. this.setParallelInfo(options);
  133. // Push empty yAxes in case user did not define them:
  134. for (; yAxisLength <= this.parallelInfo.counter; yAxisLength++) {
  135. newYAxes.push({});
  136. }
  137. if (!options.legend) {
  138. options.legend = {};
  139. }
  140. if (options.legend.enabled === undefined) {
  141. options.legend.enabled = false;
  142. }
  143. merge(
  144. true,
  145. options,
  146. // Disable boost
  147. {
  148. boost: {
  149. seriesThreshold: Number.MAX_VALUE
  150. },
  151. plotOptions: {
  152. series: {
  153. boostThreshold: Number.MAX_VALUE
  154. }
  155. }
  156. }
  157. );
  158. options.yAxis = defaultyAxis.concat(newYAxes);
  159. options.xAxis = merge(
  160. defaultXAxisOptions, // docs
  161. splat(options.xAxis || {})[0]
  162. );
  163. }
  164. });
  165. // Initialize parallelCoordinates
  166. addEvent(Chart, 'update', function (e) {
  167. var options = e.options;
  168. if (options.chart) {
  169. if (defined(options.chart.parallelCoordinates)) {
  170. this.hasParallelCoordinates = options.chart.parallelCoordinates;
  171. }
  172. if (this.hasParallelCoordinates && options.chart.parallelAxes) {
  173. this.options.chart.parallelAxes = merge(
  174. this.options.chart.parallelAxes,
  175. options.chart.parallelAxes
  176. );
  177. this.yAxis.forEach(function (axis) {
  178. axis.update({}, false);
  179. });
  180. }
  181. }
  182. });
  183. extend(ChartProto, /** @lends Highcharts.Chart.prototype */ {
  184. /**
  185. * Define how many parellel axes we have according to the longest dataset.
  186. * This is quite heavy - loop over all series and check series.data.length
  187. * Consider:
  188. *
  189. * - make this an option, so user needs to set this to get better
  190. * performance
  191. *
  192. * - check only first series for number of points and assume the rest is the
  193. * same
  194. *
  195. * @private
  196. * @function Highcharts.Chart#setParallelInfo
  197. *
  198. * @param {Highcharts.Options} options
  199. * User options
  200. */
  201. setParallelInfo: function (options) {
  202. var chart = this,
  203. seriesOptions = options.series;
  204. chart.parallelInfo = {
  205. counter: 0
  206. };
  207. seriesOptions.forEach(function (series) {
  208. if (series.data) {
  209. chart.parallelInfo.counter = Math.max(
  210. chart.parallelInfo.counter,
  211. series.data.length - 1
  212. );
  213. }
  214. });
  215. }
  216. });
  217. // On update, keep parallelPosition.
  218. AxisProto.keepProps.push('parallelPosition');
  219. // Update default options with predefined for a parallel coords.
  220. addEvent(Axis, 'afterSetOptions', function (e) {
  221. var axis = this,
  222. chart = axis.chart,
  223. axisPosition = ['left', 'width', 'height', 'top'];
  224. if (chart.hasParallelCoordinates) {
  225. if (chart.inverted) {
  226. axisPosition = axisPosition.reverse();
  227. }
  228. if (axis.isXAxis) {
  229. axis.options = merge(
  230. axis.options,
  231. defaultXAxisOptions,
  232. e.userOptions
  233. );
  234. } else {
  235. axis.options = merge(
  236. axis.options,
  237. axis.chart.options.chart.parallelAxes,
  238. e.userOptions
  239. );
  240. axis.parallelPosition = pick(
  241. axis.parallelPosition,
  242. chart.yAxis.length
  243. );
  244. axis.setParallelPosition(axisPosition, axis.options);
  245. }
  246. }
  247. });
  248. /* Each axis should gather extremes from points on a particular position in
  249. series.data. Not like the default one, which gathers extremes from all series
  250. bind to this axis. Consider using series.points instead of series.yData. */
  251. addEvent(Axis, 'getSeriesExtremes', function (e) {
  252. if (this.chart && this.chart.hasParallelCoordinates && !this.isXAxis) {
  253. var index = this.parallelPosition,
  254. currentPoints = [];
  255. this.series.forEach(function (series) {
  256. if (series.visible && defined(series.yData[index])) {
  257. // We need to use push() beacause of null points
  258. currentPoints.push(series.yData[index]);
  259. }
  260. });
  261. this.dataMin = arrayMin(currentPoints);
  262. this.dataMax = arrayMax(currentPoints);
  263. e.preventDefault();
  264. }
  265. });
  266. extend(AxisProto, /** @lends Highcharts.Axis.prototype */ {
  267. /**
  268. * Set predefined left+width and top+height (inverted) for yAxes. This
  269. * method modifies options param.
  270. *
  271. * @function Highcharts.Axis#setParallelPosition
  272. *
  273. * @param {Array<string>} axisPosition
  274. * ['left', 'width', 'height', 'top'] or
  275. * ['top', 'height', 'width', 'left'] for an inverted chart.
  276. *
  277. * @param {Highcharts.AxisOptions} options
  278. * {@link Highcharts.Axis#options}.
  279. */
  280. setParallelPosition: function (axisPosition, options) {
  281. var fraction = (this.parallelPosition + 0.5) /
  282. (this.chart.parallelInfo.counter + 1);
  283. if (this.chart.polar) {
  284. options.angle = 360 * fraction;
  285. } else {
  286. options[axisPosition[0]] = 100 * fraction + '%';
  287. this[axisPosition[1]] = options[axisPosition[1]] = 0;
  288. // In case of chart.update(inverted), remove old options:
  289. this[axisPosition[2]] = options[axisPosition[2]] = null;
  290. this[axisPosition[3]] = options[axisPosition[3]] = null;
  291. }
  292. }
  293. });
  294. // Bind each series to each yAxis. yAxis needs a reference to all series to
  295. // calculate extremes.
  296. addEvent(H.Series, 'bindAxes', function (e) {
  297. if (this.chart.hasParallelCoordinates) {
  298. var series = this;
  299. this.chart.axes.forEach(function (axis) {
  300. series.insert(axis.series);
  301. axis.isDirty = true;
  302. });
  303. series.xAxis = this.chart.xAxis[0];
  304. series.yAxis = this.chart.yAxis[0];
  305. e.preventDefault();
  306. }
  307. });
  308. // Translate each point using corresponding yAxis.
  309. addEvent(H.Series, 'afterTranslate', function () {
  310. var series = this,
  311. chart = this.chart,
  312. points = series.points,
  313. dataLength = points && points.length,
  314. closestPointRangePx = Number.MAX_VALUE,
  315. lastPlotX,
  316. point,
  317. i;
  318. if (this.chart.hasParallelCoordinates) {
  319. for (i = 0; i < dataLength; i++) {
  320. point = points[i];
  321. if (defined(point.y)) {
  322. if (chart.polar) {
  323. point.plotX = chart.yAxis[i].angleRad || 0;
  324. } else if (chart.inverted) {
  325. point.plotX = (
  326. chart.plotHeight -
  327. chart.yAxis[i].top +
  328. chart.plotTop
  329. );
  330. } else {
  331. point.plotX = chart.yAxis[i].left - chart.plotLeft;
  332. }
  333. point.clientX = point.plotX;
  334. point.plotY = chart.yAxis[i]
  335. .translate(point.y, false, true, null, true);
  336. if (lastPlotX !== undefined) {
  337. closestPointRangePx = Math.min(
  338. closestPointRangePx,
  339. Math.abs(point.plotX - lastPlotX)
  340. );
  341. }
  342. lastPlotX = point.plotX;
  343. point.isInside = chart.isInsidePlot(
  344. point.plotX,
  345. point.plotY,
  346. chart.inverted
  347. );
  348. } else {
  349. point.isNull = true;
  350. }
  351. }
  352. this.closestPointRangePx = closestPointRangePx;
  353. }
  354. }, { order: 1 });
  355. // On destroy, we need to remove series from each axis.series
  356. H.addEvent(H.Series, 'destroy', function () {
  357. if (this.chart.hasParallelCoordinates) {
  358. (this.chart.axes || []).forEach(function (axis) {
  359. if (axis && axis.series) {
  360. erase(axis.series, this);
  361. axis.isDirty = axis.forceRedraw = true;
  362. }
  363. }, this);
  364. }
  365. });
  366. function addFormattedValue(proceed) {
  367. var chart = this.series && this.series.chart,
  368. config = proceed.apply(this, Array.prototype.slice.call(arguments, 1)),
  369. formattedValue,
  370. yAxisOptions,
  371. labelFormat,
  372. yAxis;
  373. if (
  374. chart &&
  375. chart.hasParallelCoordinates &&
  376. !defined(config.formattedValue)
  377. ) {
  378. yAxis = chart.yAxis[this.x];
  379. yAxisOptions = yAxis.options;
  380. labelFormat = pick(
  381. /**
  382. * Parallel coordinates only. Format that will be used for point.y
  383. * and available in [tooltip.pointFormat](#tooltip.pointFormat) as
  384. * `{point.formattedValue}`. If not set, `{point.formattedValue}`
  385. * will use other options, in this order:
  386. *
  387. * 1. [yAxis.labels.format](#yAxis.labels.format) will be used if
  388. * set
  389. *
  390. * 2. If yAxis is a category, then category name will be displayed
  391. *
  392. * 3. If yAxis is a datetime, then value will use the same format as
  393. * yAxis labels
  394. *
  395. * 4. If yAxis is linear/logarithmic type, then simple value will be
  396. * used
  397. *
  398. * @sample {highcharts}
  399. * /highcharts/parallel-coordinates/tooltipvalueformat/
  400. * Different tooltipValueFormats's
  401. *
  402. * @type {string}
  403. * @default undefined
  404. * @since 6.0.0
  405. * @product highcharts
  406. * @apioption yAxis.tooltipValueFormat
  407. */
  408. yAxisOptions.tooltipValueFormat,
  409. yAxisOptions.labels.format
  410. );
  411. if (labelFormat) {
  412. formattedValue = H.format(
  413. labelFormat,
  414. extend(
  415. this,
  416. { value: this.y }
  417. ),
  418. chart.time
  419. );
  420. } else if (yAxis.isDatetimeAxis) {
  421. formattedValue = chart.time.dateFormat(
  422. chart.time.resolveDTLFormat(yAxisOptions.dateTimeLabelFormats[
  423. yAxis.tickPositions.info.unitName
  424. ]).main,
  425. this.y
  426. );
  427. } else if (yAxisOptions.categories) {
  428. formattedValue = yAxisOptions.categories[this.y];
  429. } else {
  430. formattedValue = this.y;
  431. }
  432. config.formattedValue = config.point.formattedValue = formattedValue;
  433. }
  434. return config;
  435. }
  436. ['line', 'spline'].forEach(function (seriesName) {
  437. wrap(
  438. H.seriesTypes[seriesName].prototype.pointClass.prototype,
  439. 'getLabelConfig',
  440. addFormattedValue
  441. );
  442. });