echarts Parallel 源码
echarts Parallel 代码
文件路径:/src/coord/parallel/Parallel.ts
/*
* 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';
import ParallelModel, { ParallelLayoutDirection } from './ParallelModel';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../core/ExtensionAPI';
import { Dictionary, DimensionName, ScaleDataValue } from '../../util/types';
import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem';
import ParallelAxisModel, { ParallelActiveState } from './AxisModel';
import SeriesData from '../../data/SeriesData';
import { AxisBaseModel } from '../AxisBaseModel';
import { CategoryAxisBaseOption } from '../axisCommonTypes';
const each = zrUtil.each;
const mathMin = Math.min;
const mathMax = Math.max;
const mathFloor = Math.floor;
const mathCeil = Math.ceil;
const round = numberUtil.round;
const PI = Math.PI;
interface ParallelCoordinateSystemLayoutInfo {
layout: ParallelLayoutDirection;
pixelDimIndex: number;
layoutBase: number;
layoutLength: number;
axisBase: number;
axisLength: number;
axisExpandable: boolean;
axisExpandWidth: number;
axisCollapseWidth: number;
axisExpandWindow: number[];
axisCount: number;
winInnerIndices: number[];
axisExpandWindow0Pos: number;
}
export interface ParallelAxisLayoutInfo {
position: number[];
rotation: number;
transform: matrix.MatrixArray;
axisNameAvailableWidth: number;
axisLabelShow: boolean;
nameTruncateMaxWidth: number;
tickDirection: -1 | 1;
labelDirection: -1 | 1;
}
type SlidedAxisExpandBehavior = 'none' | 'slide' | 'jump';
class Parallel implements CoordinateSystemMaster, CoordinateSystem {
readonly type = 'parallel';
/**
* key: dimension
*/
private _axesMap = zrUtil.createHashMap<ParallelAxis>();
/**
* key: dimension
* value: {position: [], rotation, }
*/
private _axesLayout: Dictionary<ParallelAxisLayoutInfo> = {};
/**
* Always follow axis order.
*/
readonly dimensions: ParallelModel['dimensions'];
private _rect: graphic.BoundingRect;
private _model: ParallelModel;
// Inject
name: string;
model: ParallelModel;
constructor(parallelModel: ParallelModel, ecModel: GlobalModel, api: ExtensionAPI) {
this.dimensions = parallelModel.dimensions;
this._model = parallelModel;
this._init(parallelModel, ecModel, api);
}
private _init(parallelModel: ParallelModel, ecModel: GlobalModel, api: ExtensionAPI): void {
const dimensions = parallelModel.dimensions;
const parallelAxisIndex = parallelModel.parallelAxisIndex;
each(dimensions, function (dim, idx) {
const axisIndex = parallelAxisIndex[idx];
const axisModel = ecModel.getComponent('parallelAxis', axisIndex) as ParallelAxisModel;
const axis = this._axesMap.set(dim, new ParallelAxis(
dim,
axisHelper.createScaleByModel(axisModel),
[0, 0],
axisModel.get('type'),
axisIndex
));
const isCategory = axis.type === 'category';
axis.onBand = isCategory
&& (axisModel as AxisBaseModel<CategoryAxisBaseOption>).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
*/
update(ecModel: GlobalModel, api: ExtensionAPI): void {
this._updateAxesFromSeries(this._model, ecModel);
}
containPoint(point: number[]): boolean {
const layoutInfo = this._makeLayoutInfo();
const axisBase = layoutInfo.axisBase;
const layoutBase = layoutInfo.layoutBase;
const pixelDimIndex = layoutInfo.pixelDimIndex;
const pAxis = point[1 - pixelDimIndex];
const pLayout = point[pixelDimIndex];
return pAxis >= axisBase
&& pAxis <= axisBase + layoutInfo.axisLength
&& pLayout >= layoutBase
&& pLayout <= layoutBase + layoutInfo.layoutLength;
}
getModel(): ParallelModel {
return this._model;
}
/**
* Update properties from series
*/
private _updateAxesFromSeries(parallelModel: ParallelModel, ecModel: GlobalModel): void {
ecModel.eachSeries(function (seriesModel) {
if (!parallelModel.contains(seriesModel, ecModel)) {
return;
}
const data = seriesModel.getData();
each(this.dimensions, function (dim) {
const 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.
*/
resize(parallelModel: ParallelModel, api: ExtensionAPI): void {
this._rect = layoutUtil.getLayoutRect(
parallelModel.getBoxLayoutParams(),
{
width: api.getWidth(),
height: api.getHeight()
}
);
this._layoutAxes();
}
getRect(): graphic.BoundingRect {
return this._rect;
}
private _makeLayoutInfo(): ParallelCoordinateSystemLayoutInfo {
const parallelModel = this._model;
const rect = this._rect;
const xy = ['x', 'y'] as const;
const wh = ['width', 'height'] as const;
const layout = parallelModel.get('layout');
const pixelDimIndex = layout === 'horizontal' ? 0 : 1;
const layoutLength = rect[wh[pixelDimIndex]];
const layoutExtent = [0, layoutLength];
const axisCount = this.dimensions.length;
const axisExpandWidth = restrict(parallelModel.get('axisExpandWidth'), layoutExtent);
const axisExpandCount = restrict(parallelModel.get('axisExpandCount') || 0, [0, axisCount]);
const 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.
let axisExpandWindow = parallelModel.get('axisExpandWindow');
let winSize;
if (!axisExpandWindow) {
winSize = restrict(axisExpandWidth * (axisExpandCount - 1), layoutExtent);
const 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;
}
let 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].
const winInnerIndices = [
mathFloor(round(axisExpandWindow[0] / axisExpandWidth, 1)) + 1,
mathCeil(round(axisExpandWindow[1] / axisExpandWidth, 1)) - 1
];
// Pos in ec coordinates.
const 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(): void {
const rect = this._rect;
const axes = this._axesMap;
const dimensions = this.dimensions;
const layoutInfo = this._makeLayoutInfo();
const layout = layoutInfo.layout;
axes.each(function (axis) {
const axisExtent = [0, layoutInfo.axisLength];
const idx = axis.inverse ? 1 : 0;
axis.setExtent(axisExtent[idx], axisExtent[1 - idx]);
});
each(dimensions, function (dim, idx) {
const posInfo = (layoutInfo.axisExpandable
? layoutAxisWithExpand : layoutAxisWithoutExpand
)(idx, layoutInfo);
const positionTable = {
horizontal: {
x: posInfo.position,
y: layoutInfo.axisLength
},
vertical: {
x: 0,
y: posInfo.position
}
};
const rotationTable = {
horizontal: PI / 2,
vertical: 0
};
const position = [
positionTable[layout].x + rect.x,
positionTable[layout].y + rect.y
];
const rotation = rotationTable[layout];
const transform = matrix.create();
matrix.rotate(transform, transform, rotation);
matrix.translate(transform, transform, position);
// TODO
// tick layout info
// TODO
// update dimensions info based on axis order.
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.
*/
getAxis(dim: DimensionName): ParallelAxis {
return this._axesMap.get(dim);
}
/**
* Convert a dim value of a single item of series data to Point.
*/
dataToPoint(value: ScaleDataValue, dim: DimensionName): number[] {
return this.axisCoordToPoint(
this._axesMap.get(dim).dataToCoord(value),
dim
);
}
/**
* Travel data for one time, get activeState of each data item.
* @param start the start dataIndex that travel from.
* @param end the next dataIndex of the last dataIndex will be travel.
*/
eachActiveState(
data: SeriesData,
callback: (activeState: ParallelActiveState, dataIndex: number) => void,
start?: number,
end?: number
): void {
start == null && (start = 0);
end == null && (end = data.count());
const axesMap = this._axesMap;
const dimensions = this.dimensions;
const dataDimensions = [] as DimensionName[];
const axisModels = [] as ParallelAxisModel[];
zrUtil.each(dimensions, function (axisDim) {
dataDimensions.push(data.mapDimension(axisDim));
axisModels.push(axesMap.get(axisDim).model);
});
const hasActiveSet = this.hasAxisBrushed();
for (let dataIndex = start; dataIndex < end; dataIndex++) {
let activeState: ParallelActiveState;
if (!hasActiveSet) {
activeState = 'normal';
}
else {
activeState = 'active';
const values = data.getValues(dataDimensions, dataIndex);
for (let j = 0, lenj = dimensions.length; j < lenj; j++) {
const state = axisModels[j].getActiveState(values[j]);
if (state === 'inactive') {
activeState = 'inactive';
break;
}
}
}
callback(activeState, dataIndex);
}
}
/**
* Whether has any activeSet.
*/
hasAxisBrushed(): boolean {
const dimensions = this.dimensions;
const axesMap = this._axesMap;
let hasActiveSet = false;
for (let 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]
*/
axisCoordToPoint(coord: number, dim: DimensionName): number[] {
const axisLayout = this._axesLayout[dim];
return graphic.applyTransform([coord, 0], axisLayout.transform);
}
/**
* Get axis layout.
*/
getAxisLayout(dim: DimensionName): ParallelAxisLayoutInfo {
return zrUtil.clone(this._axesLayout[dim]);
}
/**
* @return {Object} {axisExpandWindow, delta, behavior: 'jump' | 'slide' | 'none'}.
*/
getSlidedAxisExpandWindow(point: number[]): {
axisExpandWindow: number[],
behavior: SlidedAxisExpandBehavior
} {
const layoutInfo = this._makeLayoutInfo();
const pixelDimIndex = layoutInfo.pixelDimIndex;
let axisExpandWindow = layoutInfo.axisExpandWindow.slice();
const winSize = axisExpandWindow[1] - axisExpandWindow[0];
const 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.
const 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.
let delta;
let behavior: SlidedAxisExpandBehavior = 'slide';
const axisCollapseWidth = layoutInfo.axisCollapseWidth;
const triggerArea = this._model.get('axisExpandSlideTriggerArea');
// But consider touch device, jump is necessary.
const 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 {
const winSize2 = axisExpandWindow[1] - axisExpandWindow[0];
const pos = extent[1] * pointCoord / winSize2;
axisExpandWindow = [mathMax(0, pos - winSize2 / 2)];
axisExpandWindow[1] = mathMin(extent[1], axisExpandWindow[0] + winSize2);
axisExpandWindow[0] = axisExpandWindow[1] - winSize2;
}
return {
axisExpandWindow: axisExpandWindow,
behavior: behavior
};
}
// TODO
// convertToPixel
// convertFromPixel
// Note:
// (1) Consider Parallel, the return type of `convertToPixel` could be number[][] (Point[]).
// (2) In parallel coord sys, how to make `convertFromPixel` make sense?
// Perhaps convert a point based on "a rensent most axis" is more meaningful rather than based on all axes?
}
function restrict(len: number, extent: number[]): number {
return mathMin(mathMax(len, extent[0]), extent[1]);
}
interface ParallelAxisLayoutPositionInfo {
position: number;
axisNameAvailableWidth: number;
axisLabelShow: boolean;
nameTruncateMaxWidth?: number;
}
function layoutAxisWithoutExpand(
axisIndex: number,
layoutInfo: ParallelCoordinateSystemLayoutInfo
): ParallelAxisLayoutPositionInfo {
const step = layoutInfo.layoutLength / (layoutInfo.axisCount - 1);
return {
position: step * axisIndex,
axisNameAvailableWidth: step,
axisLabelShow: true
};
}
function layoutAxisWithExpand(
axisIndex: number,
layoutInfo: ParallelCoordinateSystemLayoutInfo
): ParallelAxisLayoutPositionInfo {
const layoutLength = layoutInfo.layoutLength;
const axisExpandWidth = layoutInfo.axisExpandWidth;
const axisCount = layoutInfo.axisCount;
const axisCollapseWidth = layoutInfo.axisCollapseWidth;
const winInnerIndices = layoutInfo.winInnerIndices;
let position;
let axisNameAvailableWidth = axisCollapseWidth;
let axisLabelShow = false;
let 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;
相关信息
相关文章
0
赞
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦