blob: cd1761bf0029097ebe17b1a3c203ee1413e7b137 [file] [log] [blame]
/*
* 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.
*/
import Graph, { GraphEdge, GraphNode } from '../../data/Graph';
import { sub, VectorArray } from 'zrender/src/core/vector';
import { assert, bind, each, retrieve2 } from 'zrender/src/core/util';
import { GraphEdgeItemOption } from './GraphSeries';
import { getSymbolSize } from './graphHelper';
type Radian = number;
type NodeAttrOnEdge = 'node1' | 'node2';
interface EdgeWrap {
/**
* Vector of the tangent line at the center node.
*/
tangentVec: VectorArray;
/**
* Radian of tangentVec (to x positive) of tangentVec. [-Math.PI / 2, Math.PI / 2].
*/
radToXPosi: Radian;
}
interface SectionWrap {
/**
* Radian of tangentVec (to x positive) of tangentVec. [-Math.PI / 2, Math.PI / 2].
* Make sure radToXPosiStart <= radToXPosiEnd.
*/
radToXPosiStart: Radian;
radToXPosiEnd: Radian;
/**
* The count of edges that assign to this section.
*/
edgeCount: number;
}
const MATH_PI = Math.PI;
const MATH_2PI = MATH_PI * 2;
/**
* This is the radian of the intersection angle of the two control
* point of a self-loop cubic bezier edge.
* If the angle is bigger or smaller, the cubic curve is not pretty.
*/
const MAX_EDGE_SECTION_RADIAN = MATH_PI - getRadianToXPositive([4, 5.5]) * 2;
const MIN_EDGE_SECTION_RADIAN = MATH_PI / 3;
/**
* @caution This method should only be called after all
* nodes and non-self-loop edges layout finished.
*
* @note [Self-loop edge layout strategy]:
* To make it have good looking when there are muliple self-loop edges,
* place them from the biggest angle (> 60 degree) one by one.
* If there is no enough angles, put mulitple edges in one angle
* and use different curvenesses.
*
* @pending Should the section angle and self-loop edge direction be able to set by user?
* `curveness` can not express it.
*
* @pending Consider the self-loop edge might overlow the canvas.
* When calculating the view transform, there is no self-loop layout info yet.
*/
export function layoutSelfLoopEdges(
graph: Graph,
// Get from `getNodeGlobalScale(seriesModel)`
nodeScaleOnCoordSys: number
): void {
graph.eachNode(node => {
const selfLoopEdges: GraphEdge[] = [];
// inEdges includes outEdges if self-loop.
each(node.inEdges, edge => {
if (isSelfLoopEdge(edge)) {
selfLoopEdges.push(edge);
}
});
if (selfLoopEdges.length) {
const sectionList = prepareSectionList(node, selfLoopEdges.length);
placeSelfLoopEdges(node, sectionList, selfLoopEdges, nodeScaleOnCoordSys);
}
});
}
/**
* @return Sections that can arrange self-loop angles. Ensure that:
* `selfLoopEdgeCount <= sectionList.reduce((sum, sec) === sec.edgeCount + sum, 0)`
*/
function prepareSectionList(centerNode: GraphNode, selfLoopEdgeCount: number): SectionWrap[] {
const adjacentEdges: EdgeWrap[] = [];
function addAdjacentEdge(centerNodeAttr: NodeAttrOnEdge, edge: GraphEdge): void {
if (isSelfLoopEdge(edge)) {
return;
}
const tangentVec = getTangentVector(edge, centerNodeAttr);
const radToXPosi = getRadianToXPositive(tangentVec);
adjacentEdges.push({ tangentVec, radToXPosi });
}
each(centerNode.inEdges, bind(addAdjacentEdge, null, 'node2'));
each(centerNode.outEdges, bind(addAdjacentEdge, null, 'node1'));
// Sort by radian asc.
adjacentEdges.sort((edgeA, edgeB) => edgeA.radToXPosi - edgeB.radToXPosi);
let availableEdgeCount = 0;
const sectionList: SectionWrap[] = [];
for (let i = 0, len = adjacentEdges.length; i < len; i++) {
const radToXPosiStart = adjacentEdges[i].radToXPosi;
const radToXPosiEnd = i < len - 1
? adjacentEdges[i + 1].radToXPosi
: adjacentEdges[0].radToXPosi + MATH_2PI;
// Make sure radToXPosiStart <= radToXPosiEnd.
const rad2Minus1 = radToXPosiEnd - radToXPosiStart;
if (rad2Minus1 >= MIN_EDGE_SECTION_RADIAN) {
sectionList.push({ radToXPosiStart, radToXPosiEnd, edgeCount: 0 });
}
availableEdgeCount += rad2Minus1 / MIN_EDGE_SECTION_RADIAN;
}
if (availableEdgeCount >= selfLoopEdgeCount) {
for (let iEdge = 0; iEdge < selfLoopEdgeCount; iEdge++) {
// Find the largest section to arrange an edge.
let iSecInMax = 0;
let secRadInMax = 0;
for (let iSec = 0; iSec < sectionList.length; iSec++) {
const thisSec = sectionList[iSec];
// If a section is too larger than anohter section, split that large section and
// arrange multiple edges in it is probably better then arrange only one edge in
// the large section.
const rad = (thisSec.radToXPosiEnd - thisSec.radToXPosiStart) / (thisSec.edgeCount + 1);
if (rad > secRadInMax) {
secRadInMax = rad;
iSecInMax = iSec;
}
}
sectionList[iSecInMax].edgeCount++;
}
}
// In this case there are probably too many edge on a node, and intersection between
// edges can not avoid. So we do not care about intersection any more.
else {
sectionList.length = 0;
sectionList.push({
radToXPosiStart: -MATH_PI / 2,
radToXPosiEnd: -MATH_PI / 2 + MATH_2PI,
edgeCount: selfLoopEdgeCount
});
}
return sectionList;
}
/**
* @return cubic bezier curve: [p1, p2, cp1, cp2]
*/
function placeSelfLoopEdges(
centerNode: GraphNode,
sectionList: SectionWrap[],
selfLoopEdges: GraphEdge[],
nodeScaleOnCoordSys: number
): void {
const symbolSize = getSymbolSize(centerNode);
const centerPt = centerNode.getLayout();
function getCubicControlPoint(radToXPosi: number, cpDistToCenter: number): number[] {
return [
Math.cos(radToXPosi) * cpDistToCenter + centerPt[0],
Math.sin(radToXPosi) * cpDistToCenter + centerPt[1]
];
};
let iEdge = 0;
each(sectionList, section => {
const secEdgeCount = section.edgeCount;
if (!secEdgeCount) {
// No self-loop edge arranged in this section.
return;
}
const secRadStart = section.radToXPosiStart;
const secRadEnd = section.radToXPosiEnd;
const splitRadHalfSpan = (secRadEnd - secRadStart) / section.edgeCount / 2;
const edgeRadHalfSpan = Math.min(splitRadHalfSpan, MAX_EDGE_SECTION_RADIAN / 2);
// const radMid = secRadStart + secRadSpan / section.edgeCount * (iEdge - iEdgeFirstInSec);
for (let iEdgeInSec = 0; iEdgeInSec < section.edgeCount; iEdgeInSec++) {
const edge = selfLoopEdges[iEdge++];
const cpMidRad = secRadStart + splitRadHalfSpan * (iEdgeInSec * 2 + 1);
// This is a experimental strategy to make it look better:
// If the symbol size is small, the bezier control point need to be far from the
// center to make the buckle obvious, while if the symbol size is big, the control
// ponit should not too far to make the buckle too significant.
// So we alway make control point dist to symbol radius `100`, and enable users to
// use option `curveness` to adjust it.
// Becuase at present we do not layout multiple self-loop edges into single
// `[cp1Rad, cp2Rad]`, we do not use option `autoCurveness`.
const curveness = retrieve2(
edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']),
0
);
const cpDistToCenter = (symbolSize / 2 + 100) * nodeScaleOnCoordSys
* (curveness + 1)
// Formula:
// If `cpDistToCenter = symbolSize / 2 * nodeScaleOnCoordSys / 3 * 4 / Math.cos(edgeRadHalfSpan)`,
// the control point can be tangent to the symbol circle.
// Hint: `distCubicMiddlePtToCenterPt / 3 * 4` get the hight of the isosceles triangle made by
// control points and center point.
/ 3 * 4 / Math.cos(edgeRadHalfSpan);
edge.setLayout([
centerPt.slice(),
centerPt.slice(),
getCubicControlPoint(cpMidRad - edgeRadHalfSpan, cpDistToCenter),
getCubicControlPoint(cpMidRad + edgeRadHalfSpan, cpDistToCenter)
]);
}
});
assert(iEdge === selfLoopEdges.length);
}
/**
* @return vector representing the tangant line
* (from edge['node1' | 'node2'] to cp1 of the cubic bezier curve)
*/
function getTangentVector(edge: GraphEdge, nodeAttr: NodeAttrOnEdge): VectorArray {
// points is [p1, p2] or [p1, p2, cp1].
const points = edge.getLayout();
const targetPt = points[2] ? points[2] : points[1];
return sub([], targetPt, edge[nodeAttr].getLayout());
}
function getRadianToXPositive(vec: VectorArray): Radian {
return Math.atan2(vec[1], vec[0]);
}
export function isSelfLoopEdge(edge: GraphEdge): boolean {
return edge.node1 === edge.node2;
}