ParallelCoordinates.js 16 KB

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