Spaces:
Running
Running
/* | |
* 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; | |
}); | |
}); | |
} |