Parallel.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. var zrUtil = require("zrender/lib/core/util");
  2. var matrix = require("zrender/lib/core/matrix");
  3. var layoutUtil = require("../../util/layout");
  4. var axisHelper = require("../../coord/axisHelper");
  5. var ParallelAxis = require("./ParallelAxis");
  6. var graphic = require("../../util/graphic");
  7. var numberUtil = require("../../util/number");
  8. var sliderMove = require("../../component/helper/sliderMove");
  9. /**
  10. * Parallel Coordinates
  11. * <https://en.wikipedia.org/wiki/Parallel_coordinates>
  12. */
  13. var each = zrUtil.each;
  14. var mathMin = Math.min;
  15. var mathMax = Math.max;
  16. var mathFloor = Math.floor;
  17. var mathCeil = Math.ceil;
  18. var round = numberUtil.round;
  19. var PI = Math.PI;
  20. function Parallel(parallelModel, ecModel, api) {
  21. /**
  22. * key: dimension
  23. * @type {Object.<string, module:echarts/coord/parallel/Axis>}
  24. * @private
  25. */
  26. this._axesMap = zrUtil.createHashMap();
  27. /**
  28. * key: dimension
  29. * value: {position: [], rotation, }
  30. * @type {Object.<string, Object>}
  31. * @private
  32. */
  33. this._axesLayout = {};
  34. /**
  35. * Always follow axis order.
  36. * @type {Array.<string>}
  37. * @readOnly
  38. */
  39. this.dimensions = parallelModel.dimensions;
  40. /**
  41. * @type {module:zrender/core/BoundingRect}
  42. */
  43. this._rect;
  44. /**
  45. * @type {module:echarts/coord/parallel/ParallelModel}
  46. */
  47. this._model = parallelModel;
  48. this._init(parallelModel, ecModel, api);
  49. }
  50. Parallel.prototype = {
  51. type: 'parallel',
  52. constructor: Parallel,
  53. /**
  54. * Initialize cartesian coordinate systems
  55. * @private
  56. */
  57. _init: function (parallelModel, ecModel, api) {
  58. var dimensions = parallelModel.dimensions;
  59. var parallelAxisIndex = parallelModel.parallelAxisIndex;
  60. each(dimensions, function (dim, idx) {
  61. var axisIndex = parallelAxisIndex[idx];
  62. var axisModel = ecModel.getComponent('parallelAxis', axisIndex);
  63. var axis = this._axesMap.set(dim, new ParallelAxis(dim, axisHelper.createScaleByModel(axisModel), [0, 0], axisModel.get('type'), axisIndex));
  64. var isCategory = axis.type === 'category';
  65. axis.onBand = isCategory && axisModel.get('boundaryGap');
  66. axis.inverse = axisModel.get('inverse'); // Injection
  67. axisModel.axis = axis;
  68. axis.model = axisModel;
  69. axis.coordinateSystem = axisModel.coordinateSystem = this;
  70. }, this);
  71. },
  72. /**
  73. * Update axis scale after data processed
  74. * @param {module:echarts/model/Global} ecModel
  75. * @param {module:echarts/ExtensionAPI} api
  76. */
  77. update: function (ecModel, api) {
  78. this._updateAxesFromSeries(this._model, ecModel);
  79. },
  80. /**
  81. * @override
  82. */
  83. containPoint: function (point) {
  84. var layoutInfo = this._makeLayoutInfo();
  85. var axisBase = layoutInfo.axisBase;
  86. var layoutBase = layoutInfo.layoutBase;
  87. var pixelDimIndex = layoutInfo.pixelDimIndex;
  88. var pAxis = point[1 - pixelDimIndex];
  89. var pLayout = point[pixelDimIndex];
  90. return pAxis >= axisBase && pAxis <= axisBase + layoutInfo.axisLength && pLayout >= layoutBase && pLayout <= layoutBase + layoutInfo.layoutLength;
  91. },
  92. getModel: function () {
  93. return this._model;
  94. },
  95. /**
  96. * Update properties from series
  97. * @private
  98. */
  99. _updateAxesFromSeries: function (parallelModel, ecModel) {
  100. ecModel.eachSeries(function (seriesModel) {
  101. if (!parallelModel.contains(seriesModel, ecModel)) {
  102. return;
  103. }
  104. var data = seriesModel.getData();
  105. each(this.dimensions, function (dim) {
  106. var axis = this._axesMap.get(dim);
  107. axis.scale.unionExtentFromData(data, dim);
  108. axisHelper.niceScaleExtent(axis.scale, axis.model);
  109. }, this);
  110. }, this);
  111. },
  112. /**
  113. * Resize the parallel coordinate system.
  114. * @param {module:echarts/coord/parallel/ParallelModel} parallelModel
  115. * @param {module:echarts/ExtensionAPI} api
  116. */
  117. resize: function (parallelModel, api) {
  118. this._rect = layoutUtil.getLayoutRect(parallelModel.getBoxLayoutParams(), {
  119. width: api.getWidth(),
  120. height: api.getHeight()
  121. });
  122. this._layoutAxes();
  123. },
  124. /**
  125. * @return {module:zrender/core/BoundingRect}
  126. */
  127. getRect: function () {
  128. return this._rect;
  129. },
  130. /**
  131. * @private
  132. */
  133. _makeLayoutInfo: function () {
  134. var parallelModel = this._model;
  135. var rect = this._rect;
  136. var xy = ['x', 'y'];
  137. var wh = ['width', 'height'];
  138. var layout = parallelModel.get('layout');
  139. var pixelDimIndex = layout === 'horizontal' ? 0 : 1;
  140. var layoutLength = rect[wh[pixelDimIndex]];
  141. var layoutExtent = [0, layoutLength];
  142. var axisCount = this.dimensions.length;
  143. var axisExpandWidth = restrict(parallelModel.get('axisExpandWidth'), layoutExtent);
  144. var axisExpandCount = restrict(parallelModel.get('axisExpandCount') || 0, [0, axisCount]);
  145. var axisExpandable = parallelModel.get('axisExpandable') && axisCount > 3 && axisCount > axisExpandCount && axisExpandCount > 1 && axisExpandWidth > 0 && layoutLength > 0; // `axisExpandWindow` is According to the coordinates of [0, axisExpandLength],
  146. // for sake of consider the case that axisCollapseWidth is 0 (when screen is narrow),
  147. // where collapsed axes should be overlapped.
  148. var axisExpandWindow = parallelModel.get('axisExpandWindow');
  149. var winSize;
  150. if (!axisExpandWindow) {
  151. winSize = restrict(axisExpandWidth * (axisExpandCount - 1), layoutExtent);
  152. var axisExpandCenter = parallelModel.get('axisExpandCenter') || mathFloor(axisCount / 2);
  153. axisExpandWindow = [axisExpandWidth * axisExpandCenter - winSize / 2];
  154. axisExpandWindow[1] = axisExpandWindow[0] + winSize;
  155. } else {
  156. winSize = restrict(axisExpandWindow[1] - axisExpandWindow[0], layoutExtent);
  157. axisExpandWindow[1] = axisExpandWindow[0] + winSize;
  158. }
  159. var axisCollapseWidth = (layoutLength - winSize) / (axisCount - axisExpandCount); // Avoid axisCollapseWidth is too small.
  160. axisCollapseWidth < 3 && (axisCollapseWidth = 0); // Find the first and last indices > ewin[0] and < ewin[1].
  161. var winInnerIndices = [mathFloor(round(axisExpandWindow[0] / axisExpandWidth, 1)) + 1, mathCeil(round(axisExpandWindow[1] / axisExpandWidth, 1)) - 1]; // Pos in ec coordinates.
  162. var axisExpandWindow0Pos = axisCollapseWidth / axisExpandWidth * axisExpandWindow[0];
  163. return {
  164. layout: layout,
  165. pixelDimIndex: pixelDimIndex,
  166. layoutBase: rect[xy[pixelDimIndex]],
  167. layoutLength: layoutLength,
  168. axisBase: rect[xy[1 - pixelDimIndex]],
  169. axisLength: rect[wh[1 - pixelDimIndex]],
  170. axisExpandable: axisExpandable,
  171. axisExpandWidth: axisExpandWidth,
  172. axisCollapseWidth: axisCollapseWidth,
  173. axisExpandWindow: axisExpandWindow,
  174. axisCount: axisCount,
  175. winInnerIndices: winInnerIndices,
  176. axisExpandWindow0Pos: axisExpandWindow0Pos
  177. };
  178. },
  179. /**
  180. * @private
  181. */
  182. _layoutAxes: function () {
  183. var rect = this._rect;
  184. var axes = this._axesMap;
  185. var dimensions = this.dimensions;
  186. var layoutInfo = this._makeLayoutInfo();
  187. var layout = layoutInfo.layout;
  188. axes.each(function (axis) {
  189. var axisExtent = [0, layoutInfo.axisLength];
  190. var idx = axis.inverse ? 1 : 0;
  191. axis.setExtent(axisExtent[idx], axisExtent[1 - idx]);
  192. });
  193. each(dimensions, function (dim, idx) {
  194. var posInfo = (layoutInfo.axisExpandable ? layoutAxisWithExpand : layoutAxisWithoutExpand)(idx, layoutInfo);
  195. var positionTable = {
  196. horizontal: {
  197. x: posInfo.position,
  198. y: layoutInfo.axisLength
  199. },
  200. vertical: {
  201. x: 0,
  202. y: posInfo.position
  203. }
  204. };
  205. var rotationTable = {
  206. horizontal: PI / 2,
  207. vertical: 0
  208. };
  209. var position = [positionTable[layout].x + rect.x, positionTable[layout].y + rect.y];
  210. var rotation = rotationTable[layout];
  211. var transform = matrix.create();
  212. matrix.rotate(transform, transform, rotation);
  213. matrix.translate(transform, transform, position); // TODO
  214. // tick等排布信息。
  215. // TODO
  216. // 根据axis order 更新 dimensions顺序。
  217. this._axesLayout[dim] = {
  218. position: position,
  219. rotation: rotation,
  220. transform: transform,
  221. axisNameAvailableWidth: posInfo.axisNameAvailableWidth,
  222. axisLabelShow: posInfo.axisLabelShow,
  223. nameTruncateMaxWidth: posInfo.nameTruncateMaxWidth,
  224. tickDirection: 1,
  225. labelDirection: 1,
  226. labelInterval: axes.get(dim).getLabelInterval()
  227. };
  228. }, this);
  229. },
  230. /**
  231. * Get axis by dim.
  232. * @param {string} dim
  233. * @return {module:echarts/coord/parallel/ParallelAxis} [description]
  234. */
  235. getAxis: function (dim) {
  236. return this._axesMap.get(dim);
  237. },
  238. /**
  239. * Convert a dim value of a single item of series data to Point.
  240. * @param {*} value
  241. * @param {string} dim
  242. * @return {Array}
  243. */
  244. dataToPoint: function (value, dim) {
  245. return this.axisCoordToPoint(this._axesMap.get(dim).dataToCoord(value), dim);
  246. },
  247. /**
  248. * Travel data for one time, get activeState of each data item.
  249. * @param {module:echarts/data/List} data
  250. * @param {Functio} cb param: {string} activeState 'active' or 'inactive' or 'normal'
  251. * {number} dataIndex
  252. * @param {Object} context
  253. */
  254. eachActiveState: function (data, callback, context) {
  255. var dimensions = this.dimensions;
  256. var axesMap = this._axesMap;
  257. var hasActiveSet = this.hasAxisBrushed();
  258. for (var i = 0, len = data.count(); i < len; i++) {
  259. var values = data.getValues(dimensions, i);
  260. var activeState;
  261. if (!hasActiveSet) {
  262. activeState = 'normal';
  263. } else {
  264. activeState = 'active';
  265. for (var j = 0, lenj = dimensions.length; j < lenj; j++) {
  266. var dimName = dimensions[j];
  267. var state = axesMap.get(dimName).model.getActiveState(values[j], j);
  268. if (state === 'inactive') {
  269. activeState = 'inactive';
  270. break;
  271. }
  272. }
  273. }
  274. callback.call(context, activeState, i);
  275. }
  276. },
  277. /**
  278. * Whether has any activeSet.
  279. * @return {boolean}
  280. */
  281. hasAxisBrushed: function () {
  282. var dimensions = this.dimensions;
  283. var axesMap = this._axesMap;
  284. var hasActiveSet = false;
  285. for (var j = 0, lenj = dimensions.length; j < lenj; j++) {
  286. if (axesMap.get(dimensions[j]).model.getActiveState() !== 'normal') {
  287. hasActiveSet = true;
  288. }
  289. }
  290. return hasActiveSet;
  291. },
  292. /**
  293. * Convert coords of each axis to Point.
  294. * Return point. For example: [10, 20]
  295. * @param {Array.<number>} coords
  296. * @param {string} dim
  297. * @return {Array.<number>}
  298. */
  299. axisCoordToPoint: function (coord, dim) {
  300. var axisLayout = this._axesLayout[dim];
  301. return graphic.applyTransform([coord, 0], axisLayout.transform);
  302. },
  303. /**
  304. * Get axis layout.
  305. */
  306. getAxisLayout: function (dim) {
  307. return zrUtil.clone(this._axesLayout[dim]);
  308. },
  309. /**
  310. * @param {Array.<number>} point
  311. * @return {Object} {axisExpandWindow, delta, behavior: 'jump' | 'slide' | 'none'}.
  312. */
  313. getSlidedAxisExpandWindow: function (point) {
  314. var layoutInfo = this._makeLayoutInfo();
  315. var pixelDimIndex = layoutInfo.pixelDimIndex;
  316. var axisExpandWindow = layoutInfo.axisExpandWindow.slice();
  317. var winSize = axisExpandWindow[1] - axisExpandWindow[0];
  318. var extent = [0, layoutInfo.axisExpandWidth * (layoutInfo.axisCount - 1)]; // Out of the area of coordinate system.
  319. if (!this.containPoint(point)) {
  320. return {
  321. behavior: 'none',
  322. axisExpandWindow: axisExpandWindow
  323. };
  324. } // Conver the point from global to expand coordinates.
  325. var pointCoord = point[pixelDimIndex] - layoutInfo.layoutBase - layoutInfo.axisExpandWindow0Pos; // For dragging operation convenience, the window should not be
  326. // slided when mouse is the center area of the window.
  327. var delta;
  328. var behavior = 'slide';
  329. var axisCollapseWidth = layoutInfo.axisCollapseWidth;
  330. var triggerArea = this._model.get('axisExpandSlideTriggerArea'); // But consider touch device, jump is necessary.
  331. var useJump = triggerArea[0] != null;
  332. if (axisCollapseWidth) {
  333. if (useJump && axisCollapseWidth && pointCoord < winSize * triggerArea[0]) {
  334. behavior = 'jump';
  335. delta = pointCoord - winSize * triggerArea[2];
  336. } else if (useJump && axisCollapseWidth && pointCoord > winSize * (1 - triggerArea[0])) {
  337. behavior = 'jump';
  338. delta = pointCoord - winSize * (1 - triggerArea[2]);
  339. } else {
  340. (delta = pointCoord - winSize * triggerArea[1]) >= 0 && (delta = pointCoord - winSize * (1 - triggerArea[1])) <= 0 && (delta = 0);
  341. }
  342. delta *= layoutInfo.axisExpandWidth / axisCollapseWidth;
  343. delta ? sliderMove(delta, axisExpandWindow, extent, 'all') // Avoid nonsense triger on mousemove.
  344. : behavior = 'none';
  345. } // When screen is too narrow, make it visible and slidable, although it is hard to interact.
  346. else {
  347. var winSize = axisExpandWindow[1] - axisExpandWindow[0];
  348. var pos = extent[1] * pointCoord / winSize;
  349. axisExpandWindow = [mathMax(0, pos - winSize / 2)];
  350. axisExpandWindow[1] = mathMin(extent[1], axisExpandWindow[0] + winSize);
  351. axisExpandWindow[0] = axisExpandWindow[1] - winSize;
  352. }
  353. return {
  354. axisExpandWindow: axisExpandWindow,
  355. behavior: behavior
  356. };
  357. }
  358. };
  359. function restrict(len, extent) {
  360. return mathMin(mathMax(len, extent[0]), extent[1]);
  361. }
  362. function layoutAxisWithoutExpand(axisIndex, layoutInfo) {
  363. var step = layoutInfo.layoutLength / (layoutInfo.axisCount - 1);
  364. return {
  365. position: step * axisIndex,
  366. axisNameAvailableWidth: step,
  367. axisLabelShow: true
  368. };
  369. }
  370. function layoutAxisWithExpand(axisIndex, layoutInfo) {
  371. var layoutLength = layoutInfo.layoutLength;
  372. var axisExpandWidth = layoutInfo.axisExpandWidth;
  373. var axisCount = layoutInfo.axisCount;
  374. var axisCollapseWidth = layoutInfo.axisCollapseWidth;
  375. var winInnerIndices = layoutInfo.winInnerIndices;
  376. var position;
  377. var axisNameAvailableWidth = axisCollapseWidth;
  378. var axisLabelShow = false;
  379. var nameTruncateMaxWidth;
  380. if (axisIndex < winInnerIndices[0]) {
  381. position = axisIndex * axisCollapseWidth;
  382. nameTruncateMaxWidth = axisCollapseWidth;
  383. } else if (axisIndex <= winInnerIndices[1]) {
  384. position = layoutInfo.axisExpandWindow0Pos + axisIndex * axisExpandWidth - layoutInfo.axisExpandWindow[0];
  385. axisNameAvailableWidth = axisExpandWidth;
  386. axisLabelShow = true;
  387. } else {
  388. position = layoutLength - (axisCount - 1 - axisIndex) * axisCollapseWidth;
  389. nameTruncateMaxWidth = axisCollapseWidth;
  390. }
  391. return {
  392. position: position,
  393. axisNameAvailableWidth: axisNameAvailableWidth,
  394. axisLabelShow: axisLabelShow,
  395. nameTruncateMaxWidth: nameTruncateMaxWidth
  396. };
  397. }
  398. var _default = Parallel;
  399. module.exports = _default;