2
0

ParallelCoordinates.js 16 KB

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