ZoeDuan's picture
Upload 1382 files
4bb817b verified
raw
history blame
18.2 kB
/*
* 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 * as layout from '../../util/layout';
import * as zrUtil from 'zrender/src/core/util';
import {groupData} from '../../util/model';
import ExtensionAPI from '../../core/ExtensionAPI';
import SankeySeriesModel, { SankeySeriesOption, SankeyNodeItemOption } from './SankeySeries';
import { GraphNode, GraphEdge } from '../../data/Graph';
import { LayoutOrient } from '../../util/types';
import GlobalModel from '../../model/Global';
export default function sankeyLayout(ecModel: GlobalModel, api: ExtensionAPI) {
ecModel.eachSeriesByType('sankey', function (seriesModel: SankeySeriesModel) {
const nodeWidth = seriesModel.get('nodeWidth');
const nodeGap = seriesModel.get('nodeGap');
const layoutInfo = getViewRect(seriesModel, api);
seriesModel.layoutInfo = layoutInfo;
const width = layoutInfo.width;
const height = layoutInfo.height;
const graph = seriesModel.getGraph();
const nodes = graph.nodes;
const edges = graph.edges;
computeNodeValues(nodes);
const filteredNodes = zrUtil.filter(nodes, function (node) {
return node.getLayout().value === 0;
});
const iterations = filteredNodes.length !== 0 ? 0 : seriesModel.get('layoutIterations');
const orient = seriesModel.get('orient');
const nodeAlign = seriesModel.get('nodeAlign');
layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign);
});
}
/**
* Get the layout position of the whole view
*/
function getViewRect(seriesModel: SankeySeriesModel, api: ExtensionAPI) {
return layout.getLayoutRect(
seriesModel.getBoxLayoutParams(), {
width: api.getWidth(),
height: api.getHeight()
}
);
}
function layoutSankey(
nodes: GraphNode[],
edges: GraphEdge[],
nodeWidth: number,
nodeGap: number,
width: number,
height: number,
iterations: number,
orient: LayoutOrient,
nodeAlign: SankeySeriesOption['nodeAlign']
) {
computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign);
computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient);
computeEdgeDepths(nodes, orient);
}
/**
* Compute the value of each node by summing the associated edge's value
*/
function computeNodeValues(nodes: GraphNode[]) {
zrUtil.each(nodes, function (node) {
const value1 = sum(node.outEdges, getEdgeValue);
const value2 = sum(node.inEdges, getEdgeValue);
const nodeRawValue = node.getValue() as number || 0;
const value = Math.max(value1, value2, nodeRawValue);
node.setLayout({value: value}, true);
});
}
/**
* Compute the x-position for each node.
*
* Here we use Kahn algorithm to detect cycle when we traverse
* the node to computer the initial x position.
*/
function computeNodeBreadths(
nodes: GraphNode[],
edges: GraphEdge[],
nodeWidth: number,
width: number,
height: number,
orient: LayoutOrient,
nodeAlign: SankeySeriesOption['nodeAlign']
) {
// Used to mark whether the edge is deleted. if it is deleted,
// the value is 0, otherwise it is 1.
const remainEdges = [];
// Storage each node's indegree.
const indegreeArr = [];
// Used to storage the node with indegree is equal to 0.
let zeroIndegrees: GraphNode[] = [];
let nextTargetNode: GraphNode[] = [];
let x = 0;
// let kx = 0;
for (let i = 0; i < edges.length; i++) {
remainEdges[i] = 1;
}
for (let i = 0; i < nodes.length; i++) {
indegreeArr[i] = nodes[i].inEdges.length;
if (indegreeArr[i] === 0) {
zeroIndegrees.push(nodes[i]);
}
}
let maxNodeDepth = -1;
// Traversing nodes using topological sorting to calculate the
// horizontal(if orient === 'horizontal') or vertical(if orient === 'vertical')
// position of the nodes.
while (zeroIndegrees.length) {
for (let idx = 0; idx < zeroIndegrees.length; idx++) {
const node = zeroIndegrees[idx];
const item = node.hostGraph.data.getRawDataItem(node.dataIndex) as SankeyNodeItemOption;
const isItemDepth = item.depth != null && item.depth >= 0;
if (isItemDepth && item.depth > maxNodeDepth) {
maxNodeDepth = item.depth;
}
node.setLayout({depth: isItemDepth ? item.depth : x}, true);
orient === 'vertical'
? node.setLayout({dy: nodeWidth}, true)
: node.setLayout({dx: nodeWidth}, true);
for (let edgeIdx = 0; edgeIdx < node.outEdges.length; edgeIdx++) {
const edge = node.outEdges[edgeIdx];
const indexEdge = edges.indexOf(edge);
remainEdges[indexEdge] = 0;
const targetNode = edge.node2;
const nodeIndex = nodes.indexOf(targetNode);
if (--indegreeArr[nodeIndex] === 0 && nextTargetNode.indexOf(targetNode) < 0) {
nextTargetNode.push(targetNode);
}
}
}
++x;
zeroIndegrees = nextTargetNode;
nextTargetNode = [];
}
for (let i = 0; i < remainEdges.length; i++) {
if (remainEdges[i] === 1) {
throw new Error('Sankey is a DAG, the original data has cycle!');
}
}
const maxDepth = maxNodeDepth > x - 1 ? maxNodeDepth : x - 1;
if (nodeAlign && nodeAlign !== 'left') {
adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth);
}
const kx = orient === 'vertical'
? (height - nodeWidth) / maxDepth
: (width - nodeWidth) / maxDepth;
scaleNodeBreadths(nodes, kx, orient);
}
function isNodeDepth(node: GraphNode) {
const item = node.hostGraph.data.getRawDataItem(node.dataIndex) as SankeyNodeItemOption;
return item.depth != null && item.depth >= 0;
}
function adjustNodeWithNodeAlign(
nodes: GraphNode[],
nodeAlign: SankeySeriesOption['nodeAlign'],
orient: LayoutOrient,
maxDepth: number
) {
if (nodeAlign === 'right') {
let nextSourceNode: GraphNode[] = [];
let remainNodes = nodes;
let nodeHeight = 0;
while (remainNodes.length) {
for (let i = 0; i < remainNodes.length; i++) {
const node = remainNodes[i];
node.setLayout({skNodeHeight: nodeHeight}, true);
for (let j = 0; j < node.inEdges.length; j++) {
const edge = node.inEdges[j];
if (nextSourceNode.indexOf(edge.node1) < 0) {
nextSourceNode.push(edge.node1);
}
}
}
remainNodes = nextSourceNode;
nextSourceNode = [];
++nodeHeight;
}
zrUtil.each(nodes, function (node) {
if (!isNodeDepth(node)) {
node.setLayout({depth: Math.max(0, maxDepth - node.getLayout().skNodeHeight)}, true);
}
});
}
else if (nodeAlign === 'justify') {
moveSinksRight(nodes, maxDepth);
}
}
/**
* All the node without outEgdes are assigned maximum x-position and
* be aligned in the last column.
*
* @param nodes. node of sankey view.
* @param maxDepth. use to assign to node without outEdges as x-position.
*/
function moveSinksRight(nodes: GraphNode[], maxDepth: number) {
zrUtil.each(nodes, function (node) {
if (!isNodeDepth(node) && !node.outEdges.length) {
node.setLayout({depth: maxDepth}, true);
}
});
}
/**
* Scale node x-position to the width
*
* @param nodes node of sankey view
* @param kx multiple used to scale nodes
*/
function scaleNodeBreadths(nodes: GraphNode[], kx: number, orient: LayoutOrient) {
zrUtil.each(nodes, function (node) {
const nodeDepth = node.getLayout().depth * kx;
orient === 'vertical'
? node.setLayout({y: nodeDepth}, true)
: node.setLayout({x: nodeDepth}, true);
});
}
/**
* Using Gauss-Seidel iterations method to compute the node depth(y-position)
*
* @param nodes node of sankey view
* @param edges edge of sankey view
* @param height the whole height of the area to draw the view
* @param nodeGap the vertical distance between two nodes
* in the same column.
* @param iterations the number of iterations for the algorithm
*/
function computeNodeDepths(
nodes: GraphNode[],
edges: GraphEdge[],
height: number,
width: number,
nodeGap: number,
iterations: number,
orient: LayoutOrient
) {
const nodesByBreadth = prepareNodesByBreadth(nodes, orient);
initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient);
resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
for (let alpha = 1; iterations > 0; iterations--) {
// 0.99 is a experience parameter, ensure that each iterations of
// changes as small as possible.
alpha *= 0.99;
relaxRightToLeft(nodesByBreadth, alpha, orient);
resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
relaxLeftToRight(nodesByBreadth, alpha, orient);
resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
}
}
function prepareNodesByBreadth(nodes: GraphNode[], orient: LayoutOrient) {
const nodesByBreadth: GraphNode[][] = [];
const keyAttr = orient === 'vertical' ? 'y' : 'x';
const groupResult = groupData(nodes, function (node) {
return node.getLayout()[keyAttr] as number;
});
groupResult.keys.sort(function (a, b) {
return a - b;
});
zrUtil.each(groupResult.keys, function (key) {
nodesByBreadth.push(groupResult.buckets.get(key));
});
return nodesByBreadth;
}
/**
* Compute the original y-position for each node
*/
function initializeNodeDepth(
nodesByBreadth: GraphNode[][],
edges: GraphEdge[],
height: number,
width: number,
nodeGap: number,
orient: LayoutOrient
) {
let minKy = Infinity;
zrUtil.each(nodesByBreadth, function (nodes) {
const n = nodes.length;
let sum = 0;
zrUtil.each(nodes, function (node) {
sum += node.getLayout().value;
});
const ky = orient === 'vertical'
? (width - (n - 1) * nodeGap) / sum
: (height - (n - 1) * nodeGap) / sum;
if (ky < minKy) {
minKy = ky;
}
});
zrUtil.each(nodesByBreadth, function (nodes) {
zrUtil.each(nodes, function (node, i) {
const nodeDy = node.getLayout().value * minKy;
if (orient === 'vertical') {
node.setLayout({x: i}, true);
node.setLayout({dx: nodeDy}, true);
}
else {
node.setLayout({y: i}, true);
node.setLayout({dy: nodeDy}, true);
}
});
});
zrUtil.each(edges, function (edge) {
const edgeDy = +edge.getValue() * minKy;
edge.setLayout({dy: edgeDy}, true);
});
}
/**
* Resolve the collision of initialized depth (y-position)
*/
function resolveCollisions(
nodesByBreadth: GraphNode[][],
nodeGap: number,
height: number,
width: number,
orient: LayoutOrient
) {
const keyAttr = orient === 'vertical' ? 'x' : 'y';
zrUtil.each(nodesByBreadth, function (nodes) {
nodes.sort(function (a, b) {
return a.getLayout()[keyAttr] - b.getLayout()[keyAttr];
});
let nodeX;
let node;
let dy;
let y0 = 0;
const n = nodes.length;
const nodeDyAttr = orient === 'vertical' ? 'dx' : 'dy';
for (let i = 0; i < n; i++) {
node = nodes[i];
dy = y0 - node.getLayout()[keyAttr];
if (dy > 0) {
nodeX = node.getLayout()[keyAttr] + dy;
orient === 'vertical'
? node.setLayout({x: nodeX}, true)
: node.setLayout({y: nodeX}, true);
}
y0 = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap;
}
const viewWidth = orient === 'vertical' ? width : height;
// If the bottommost node goes outside the bounds, push it back up
dy = y0 - nodeGap - viewWidth;
if (dy > 0) {
nodeX = node.getLayout()[keyAttr] - dy;
orient === 'vertical'
? node.setLayout({x: nodeX}, true)
: node.setLayout({y: nodeX}, true);
y0 = nodeX;
for (let i = n - 2; i >= 0; --i) {
node = nodes[i];
dy = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap - y0;
if (dy > 0) {
nodeX = node.getLayout()[keyAttr] - dy;
orient === 'vertical'
? node.setLayout({x: nodeX}, true)
: node.setLayout({y: nodeX}, true);
}
y0 = node.getLayout()[keyAttr];
}
}
});
}
/**
* Change the y-position of the nodes, except most the right side nodes
* @param nodesByBreadth
* @param alpha parameter used to adjust the nodes y-position
*/
function relaxRightToLeft(
nodesByBreadth: GraphNode[][],
alpha: number,
orient: LayoutOrient
) {
zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
zrUtil.each(nodes, function (node) {
if (node.outEdges.length) {
let y = sum(node.outEdges, weightedTarget, orient)
/ sum(node.outEdges, getEdgeValue);
if (isNaN(y)) {
const len = node.outEdges.length;
y = len ? sum(node.outEdges, centerTarget, orient) / len : 0;
}
if (orient === 'vertical') {
const nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
node.setLayout({x: nodeX}, true);
}
else {
const nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
node.setLayout({y: nodeY}, true);
}
}
});
});
}
function weightedTarget(edge: GraphEdge, orient: LayoutOrient) {
return center(edge.node2, orient) * (edge.getValue() as number);
}
function centerTarget(edge: GraphEdge, orient: LayoutOrient) {
return center(edge.node2, orient);
}
function weightedSource(edge: GraphEdge, orient: LayoutOrient) {
return center(edge.node1, orient) * (edge.getValue() as number);
}
function centerSource(edge: GraphEdge, orient: LayoutOrient) {
return center(edge.node1, orient);
}
function center(node: GraphNode, orient: LayoutOrient) {
return orient === 'vertical'
? node.getLayout().x + node.getLayout().dx / 2
: node.getLayout().y + node.getLayout().dy / 2;
}
function getEdgeValue(edge: GraphEdge) {
return edge.getValue() as number;
}
function sum<T>(array: T[], cb: (item: T, orient?: LayoutOrient) => number, orient?: LayoutOrient) {
let sum = 0;
const len = array.length;
let i = -1;
while (++i < len) {
const value = +cb(array[i], orient);
if (!isNaN(value)) {
sum += value;
}
}
return sum;
}
/**
* Change the y-position of the nodes, except most the left side nodes
*/
function relaxLeftToRight(nodesByBreadth: GraphNode[][], alpha: number, orient: LayoutOrient) {
zrUtil.each(nodesByBreadth, function (nodes) {
zrUtil.each(nodes, function (node) {
if (node.inEdges.length) {
let y = sum(node.inEdges, weightedSource, orient)
/ sum(node.inEdges, getEdgeValue);
if (isNaN(y)) {
const len = node.inEdges.length;
y = len ? sum(node.inEdges, centerSource, orient) / len : 0;
}
if (orient === 'vertical') {
const nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
node.setLayout({x: nodeX}, true);
}
else {
const nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
node.setLayout({y: nodeY}, true);
}
}
});
});
}
/**
* Compute the depth(y-position) of each edge
*/
function computeEdgeDepths(nodes: GraphNode[], orient: LayoutOrient) {
const keyAttr = orient === 'vertical' ? 'x' : 'y';
zrUtil.each(nodes, function (node) {
node.outEdges.sort(function (a, b) {
return a.node2.getLayout()[keyAttr] - b.node2.getLayout()[keyAttr];
});
node.inEdges.sort(function (a, b) {
return a.node1.getLayout()[keyAttr] - b.node1.getLayout()[keyAttr];
});
});
zrUtil.each(nodes, function (node) {
let sy = 0;
let ty = 0;
zrUtil.each(node.outEdges, function (edge) {
edge.setLayout({sy: sy}, true);
sy += edge.getLayout().dy;
});
zrUtil.each(node.inEdges, function (edge) {
edge.setLayout({ty: ty}, true);
ty += edge.getLayout().dy;
});
});
}