| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| /** |
| * Parallel Coordinates |
| * <https://en.wikipedia.org/wiki/Parallel_coordinates> |
| */ |
| import * as zrUtil from 'zrender/src/core/util'; |
| import * as matrix from 'zrender/src/core/matrix'; |
| import * as layoutUtil from '../../util/layout'; |
| import * as axisHelper from '../../coord/axisHelper'; |
| import ParallelAxis from './ParallelAxis'; |
| import * as graphic from '../../util/graphic'; |
| import * as numberUtil from '../../util/number'; |
| import sliderMove from '../../component/helper/sliderMove'; |
| var each = zrUtil.each; |
| var mathMin = Math.min; |
| var mathMax = Math.max; |
| var mathFloor = Math.floor; |
| var mathCeil = Math.ceil; |
| var round = numberUtil.round; |
| var PI = Math.PI; |
| |
| function Parallel(parallelModel, ecModel, api) { |
| /** |
| * key: dimension |
| * @type {Object.<string, module:echarts/coord/parallel/Axis>} |
| * @private |
| */ |
| this._axesMap = zrUtil.createHashMap(); |
| /** |
| * key: dimension |
| * value: {position: [], rotation, } |
| * @type {Object.<string, Object>} |
| * @private |
| */ |
| |
| this._axesLayout = {}; |
| /** |
| * Always follow axis order. |
| * @type {Array.<string>} |
| * @readOnly |
| */ |
| |
| this.dimensions = parallelModel.dimensions; |
| /** |
| * @type {module:zrender/core/BoundingRect} |
| */ |
| |
| this._rect; |
| /** |
| * @type {module:echarts/coord/parallel/ParallelModel} |
| */ |
| |
| this._model = parallelModel; |
| |
| this._init(parallelModel, ecModel, api); |
| } |
| |
| Parallel.prototype = { |
| type: 'parallel', |
| constructor: Parallel, |
| |
| /** |
| * Initialize cartesian coordinate systems |
| * @private |
| */ |
| _init: function (parallelModel, ecModel, api) { |
| var dimensions = parallelModel.dimensions; |
| var parallelAxisIndex = parallelModel.parallelAxisIndex; |
| each(dimensions, function (dim, idx) { |
| var axisIndex = parallelAxisIndex[idx]; |
| var axisModel = ecModel.getComponent('parallelAxis', axisIndex); |
| |
| var axis = this._axesMap.set(dim, new ParallelAxis(dim, axisHelper.createScaleByModel(axisModel), [0, 0], axisModel.get('type'), axisIndex)); |
| |
| var isCategory = axis.type === 'category'; |
| axis.onBand = isCategory && axisModel.get('boundaryGap'); |
| axis.inverse = axisModel.get('inverse'); // Injection |
| |
| axisModel.axis = axis; |
| axis.model = axisModel; |
| axis.coordinateSystem = axisModel.coordinateSystem = this; |
| }, this); |
| }, |
| |
| /** |
| * Update axis scale after data processed |
| * @param {module:echarts/model/Global} ecModel |
| * @param {module:echarts/ExtensionAPI} api |
| */ |
| update: function (ecModel, api) { |
| this._updateAxesFromSeries(this._model, ecModel); |
| }, |
| |
| /** |
| * @override |
| */ |
| containPoint: function (point) { |
| var layoutInfo = this._makeLayoutInfo(); |
| |
| var axisBase = layoutInfo.axisBase; |
| var layoutBase = layoutInfo.layoutBase; |
| var pixelDimIndex = layoutInfo.pixelDimIndex; |
| var pAxis = point[1 - pixelDimIndex]; |
| var pLayout = point[pixelDimIndex]; |
| return pAxis >= axisBase && pAxis <= axisBase + layoutInfo.axisLength && pLayout >= layoutBase && pLayout <= layoutBase + layoutInfo.layoutLength; |
| }, |
| getModel: function () { |
| return this._model; |
| }, |
| |
| /** |
| * Update properties from series |
| * @private |
| */ |
| _updateAxesFromSeries: function (parallelModel, ecModel) { |
| ecModel.eachSeries(function (seriesModel) { |
| if (!parallelModel.contains(seriesModel, ecModel)) { |
| return; |
| } |
| |
| var data = seriesModel.getData(); |
| each(this.dimensions, function (dim) { |
| var axis = this._axesMap.get(dim); |
| |
| axis.scale.unionExtentFromData(data, data.mapDimension(dim)); |
| axisHelper.niceScaleExtent(axis.scale, axis.model); |
| }, this); |
| }, this); |
| }, |
| |
| /** |
| * Resize the parallel coordinate system. |
| * @param {module:echarts/coord/parallel/ParallelModel} parallelModel |
| * @param {module:echarts/ExtensionAPI} api |
| */ |
| resize: function (parallelModel, api) { |
| this._rect = layoutUtil.getLayoutRect(parallelModel.getBoxLayoutParams(), { |
| width: api.getWidth(), |
| height: api.getHeight() |
| }); |
| |
| this._layoutAxes(); |
| }, |
| |
| /** |
| * @return {module:zrender/core/BoundingRect} |
| */ |
| getRect: function () { |
| return this._rect; |
| }, |
| |
| /** |
| * @private |
| */ |
| _makeLayoutInfo: function () { |
| var parallelModel = this._model; |
| var rect = this._rect; |
| var xy = ['x', 'y']; |
| var wh = ['width', 'height']; |
| var layout = parallelModel.get('layout'); |
| var pixelDimIndex = layout === 'horizontal' ? 0 : 1; |
| var layoutLength = rect[wh[pixelDimIndex]]; |
| var layoutExtent = [0, layoutLength]; |
| var axisCount = this.dimensions.length; |
| var axisExpandWidth = restrict(parallelModel.get('axisExpandWidth'), layoutExtent); |
| var axisExpandCount = restrict(parallelModel.get('axisExpandCount') || 0, [0, axisCount]); |
| var axisExpandable = parallelModel.get('axisExpandable') && axisCount > 3 && axisCount > axisExpandCount && axisExpandCount > 1 && axisExpandWidth > 0 && layoutLength > 0; // `axisExpandWindow` is According to the coordinates of [0, axisExpandLength], |
| // for sake of consider the case that axisCollapseWidth is 0 (when screen is narrow), |
| // where collapsed axes should be overlapped. |
| |
| var axisExpandWindow = parallelModel.get('axisExpandWindow'); |
| var winSize; |
| |
| if (!axisExpandWindow) { |
| winSize = restrict(axisExpandWidth * (axisExpandCount - 1), layoutExtent); |
| var axisExpandCenter = parallelModel.get('axisExpandCenter') || mathFloor(axisCount / 2); |
| axisExpandWindow = [axisExpandWidth * axisExpandCenter - winSize / 2]; |
| axisExpandWindow[1] = axisExpandWindow[0] + winSize; |
| } else { |
| winSize = restrict(axisExpandWindow[1] - axisExpandWindow[0], layoutExtent); |
| axisExpandWindow[1] = axisExpandWindow[0] + winSize; |
| } |
| |
| var axisCollapseWidth = (layoutLength - winSize) / (axisCount - axisExpandCount); // Avoid axisCollapseWidth is too small. |
| |
| axisCollapseWidth < 3 && (axisCollapseWidth = 0); // Find the first and last indices > ewin[0] and < ewin[1]. |
| |
| var winInnerIndices = [mathFloor(round(axisExpandWindow[0] / axisExpandWidth, 1)) + 1, mathCeil(round(axisExpandWindow[1] / axisExpandWidth, 1)) - 1]; // Pos in ec coordinates. |
| |
| var axisExpandWindow0Pos = axisCollapseWidth / axisExpandWidth * axisExpandWindow[0]; |
| return { |
| layout: layout, |
| pixelDimIndex: pixelDimIndex, |
| layoutBase: rect[xy[pixelDimIndex]], |
| layoutLength: layoutLength, |
| axisBase: rect[xy[1 - pixelDimIndex]], |
| axisLength: rect[wh[1 - pixelDimIndex]], |
| axisExpandable: axisExpandable, |
| axisExpandWidth: axisExpandWidth, |
| axisCollapseWidth: axisCollapseWidth, |
| axisExpandWindow: axisExpandWindow, |
| axisCount: axisCount, |
| winInnerIndices: winInnerIndices, |
| axisExpandWindow0Pos: axisExpandWindow0Pos |
| }; |
| }, |
| |
| /** |
| * @private |
| */ |
| _layoutAxes: function () { |
| var rect = this._rect; |
| var axes = this._axesMap; |
| var dimensions = this.dimensions; |
| |
| var layoutInfo = this._makeLayoutInfo(); |
| |
| var layout = layoutInfo.layout; |
| axes.each(function (axis) { |
| var axisExtent = [0, layoutInfo.axisLength]; |
| var idx = axis.inverse ? 1 : 0; |
| axis.setExtent(axisExtent[idx], axisExtent[1 - idx]); |
| }); |
| each(dimensions, function (dim, idx) { |
| var posInfo = (layoutInfo.axisExpandable ? layoutAxisWithExpand : layoutAxisWithoutExpand)(idx, layoutInfo); |
| var positionTable = { |
| horizontal: { |
| x: posInfo.position, |
| y: layoutInfo.axisLength |
| }, |
| vertical: { |
| x: 0, |
| y: posInfo.position |
| } |
| }; |
| var rotationTable = { |
| horizontal: PI / 2, |
| vertical: 0 |
| }; |
| var position = [positionTable[layout].x + rect.x, positionTable[layout].y + rect.y]; |
| var rotation = rotationTable[layout]; |
| var transform = matrix.create(); |
| matrix.rotate(transform, transform, rotation); |
| matrix.translate(transform, transform, position); // TODO |
| // tick等排布信息。 |
| // TODO |
| // 根据axis order 更新 dimensions顺序。 |
| |
| this._axesLayout[dim] = { |
| position: position, |
| rotation: rotation, |
| transform: transform, |
| axisNameAvailableWidth: posInfo.axisNameAvailableWidth, |
| axisLabelShow: posInfo.axisLabelShow, |
| nameTruncateMaxWidth: posInfo.nameTruncateMaxWidth, |
| tickDirection: 1, |
| labelDirection: 1 |
| }; |
| }, this); |
| }, |
| |
| /** |
| * Get axis by dim. |
| * @param {string} dim |
| * @return {module:echarts/coord/parallel/ParallelAxis} [description] |
| */ |
| getAxis: function (dim) { |
| return this._axesMap.get(dim); |
| }, |
| |
| /** |
| * Convert a dim value of a single item of series data to Point. |
| * @param {*} value |
| * @param {string} dim |
| * @return {Array} |
| */ |
| dataToPoint: function (value, dim) { |
| return this.axisCoordToPoint(this._axesMap.get(dim).dataToCoord(value), dim); |
| }, |
| |
| /** |
| * Travel data for one time, get activeState of each data item. |
| * @param {module:echarts/data/List} data |
| * @param {Functio} cb param: {string} activeState 'active' or 'inactive' or 'normal' |
| * {number} dataIndex |
| * @param {number} [start=0] the start dataIndex that travel from. |
| * @param {number} [end=data.count()] the next dataIndex of the last dataIndex will be travel. |
| */ |
| eachActiveState: function (data, callback, start, end) { |
| start == null && (start = 0); |
| end == null && (end = data.count()); |
| var axesMap = this._axesMap; |
| var dimensions = this.dimensions; |
| var dataDimensions = []; |
| var axisModels = []; |
| zrUtil.each(dimensions, function (axisDim) { |
| dataDimensions.push(data.mapDimension(axisDim)); |
| axisModels.push(axesMap.get(axisDim).model); |
| }); |
| var hasActiveSet = this.hasAxisBrushed(); |
| |
| for (var dataIndex = start; dataIndex < end; dataIndex++) { |
| var activeState; |
| |
| if (!hasActiveSet) { |
| activeState = 'normal'; |
| } else { |
| activeState = 'active'; |
| var values = data.getValues(dataDimensions, dataIndex); |
| |
| for (var j = 0, lenj = dimensions.length; j < lenj; j++) { |
| var state = axisModels[j].getActiveState(values[j]); |
| |
| if (state === 'inactive') { |
| activeState = 'inactive'; |
| break; |
| } |
| } |
| } |
| |
| callback(activeState, dataIndex); |
| } |
| }, |
| |
| /** |
| * Whether has any activeSet. |
| * @return {boolean} |
| */ |
| hasAxisBrushed: function () { |
| var dimensions = this.dimensions; |
| var axesMap = this._axesMap; |
| var hasActiveSet = false; |
| |
| for (var j = 0, lenj = dimensions.length; j < lenj; j++) { |
| if (axesMap.get(dimensions[j]).model.getActiveState() !== 'normal') { |
| hasActiveSet = true; |
| } |
| } |
| |
| return hasActiveSet; |
| }, |
| |
| /** |
| * Convert coords of each axis to Point. |
| * Return point. For example: [10, 20] |
| * @param {Array.<number>} coords |
| * @param {string} dim |
| * @return {Array.<number>} |
| */ |
| axisCoordToPoint: function (coord, dim) { |
| var axisLayout = this._axesLayout[dim]; |
| return graphic.applyTransform([coord, 0], axisLayout.transform); |
| }, |
| |
| /** |
| * Get axis layout. |
| */ |
| getAxisLayout: function (dim) { |
| return zrUtil.clone(this._axesLayout[dim]); |
| }, |
| |
| /** |
| * @param {Array.<number>} point |
| * @return {Object} {axisExpandWindow, delta, behavior: 'jump' | 'slide' | 'none'}. |
| */ |
| getSlidedAxisExpandWindow: function (point) { |
| var layoutInfo = this._makeLayoutInfo(); |
| |
| var pixelDimIndex = layoutInfo.pixelDimIndex; |
| var axisExpandWindow = layoutInfo.axisExpandWindow.slice(); |
| var winSize = axisExpandWindow[1] - axisExpandWindow[0]; |
| var extent = [0, layoutInfo.axisExpandWidth * (layoutInfo.axisCount - 1)]; // Out of the area of coordinate system. |
| |
| if (!this.containPoint(point)) { |
| return { |
| behavior: 'none', |
| axisExpandWindow: axisExpandWindow |
| }; |
| } // Conver the point from global to expand coordinates. |
| |
| |
| var pointCoord = point[pixelDimIndex] - layoutInfo.layoutBase - layoutInfo.axisExpandWindow0Pos; // For dragging operation convenience, the window should not be |
| // slided when mouse is the center area of the window. |
| |
| var delta; |
| var behavior = 'slide'; |
| var axisCollapseWidth = layoutInfo.axisCollapseWidth; |
| |
| var triggerArea = this._model.get('axisExpandSlideTriggerArea'); // But consider touch device, jump is necessary. |
| |
| |
| var useJump = triggerArea[0] != null; |
| |
| if (axisCollapseWidth) { |
| if (useJump && axisCollapseWidth && pointCoord < winSize * triggerArea[0]) { |
| behavior = 'jump'; |
| delta = pointCoord - winSize * triggerArea[2]; |
| } else if (useJump && axisCollapseWidth && pointCoord > winSize * (1 - triggerArea[0])) { |
| behavior = 'jump'; |
| delta = pointCoord - winSize * (1 - triggerArea[2]); |
| } else { |
| (delta = pointCoord - winSize * triggerArea[1]) >= 0 && (delta = pointCoord - winSize * (1 - triggerArea[1])) <= 0 && (delta = 0); |
| } |
| |
| delta *= layoutInfo.axisExpandWidth / axisCollapseWidth; |
| delta ? sliderMove(delta, axisExpandWindow, extent, 'all') // Avoid nonsense triger on mousemove. |
| : behavior = 'none'; |
| } // When screen is too narrow, make it visible and slidable, although it is hard to interact. |
| else { |
| var winSize = axisExpandWindow[1] - axisExpandWindow[0]; |
| var pos = extent[1] * pointCoord / winSize; |
| axisExpandWindow = [mathMax(0, pos - winSize / 2)]; |
| axisExpandWindow[1] = mathMin(extent[1], axisExpandWindow[0] + winSize); |
| axisExpandWindow[0] = axisExpandWindow[1] - winSize; |
| } |
| |
| return { |
| axisExpandWindow: axisExpandWindow, |
| behavior: behavior |
| }; |
| } |
| }; |
| |
| function restrict(len, extent) { |
| return mathMin(mathMax(len, extent[0]), extent[1]); |
| } |
| |
| function layoutAxisWithoutExpand(axisIndex, layoutInfo) { |
| var step = layoutInfo.layoutLength / (layoutInfo.axisCount - 1); |
| return { |
| position: step * axisIndex, |
| axisNameAvailableWidth: step, |
| axisLabelShow: true |
| }; |
| } |
| |
| function layoutAxisWithExpand(axisIndex, layoutInfo) { |
| var layoutLength = layoutInfo.layoutLength; |
| var axisExpandWidth = layoutInfo.axisExpandWidth; |
| var axisCount = layoutInfo.axisCount; |
| var axisCollapseWidth = layoutInfo.axisCollapseWidth; |
| var winInnerIndices = layoutInfo.winInnerIndices; |
| var position; |
| var axisNameAvailableWidth = axisCollapseWidth; |
| var axisLabelShow = false; |
| var nameTruncateMaxWidth; |
| |
| if (axisIndex < winInnerIndices[0]) { |
| position = axisIndex * axisCollapseWidth; |
| nameTruncateMaxWidth = axisCollapseWidth; |
| } else if (axisIndex <= winInnerIndices[1]) { |
| position = layoutInfo.axisExpandWindow0Pos + axisIndex * axisExpandWidth - layoutInfo.axisExpandWindow[0]; |
| axisNameAvailableWidth = axisExpandWidth; |
| axisLabelShow = true; |
| } else { |
| position = layoutLength - (axisCount - 1 - axisIndex) * axisCollapseWidth; |
| nameTruncateMaxWidth = axisCollapseWidth; |
| } |
| |
| return { |
| position: position, |
| axisNameAvailableWidth: axisNameAvailableWidth, |
| axisLabelShow: axisLabelShow, |
| nameTruncateMaxWidth: nameTruncateMaxWidth |
| }; |
| } |
| |
| export default Parallel; |