UNPKG

@antv/g2plot

Version:

An interactive and responsive charting library

232 lines (216 loc) 6.92 kB
/* * for Arc Diagram (edges without weight) / Chord Diagram (edges with source and target weight) * graph data required (nodes, edges) */ import { assign, forIn, isFunction } from '@antv/util'; import { NodeLinkData } from '../../types/relation-data'; const DEFAULT_OPTIONS: ChordLayoutOptions = { y: 0, nodeWidthRatio: 0.05, // width of the node, (0, 1) weight: false, nodePaddingRatio: 0.1, // margin ratio, [0, 1) id: (node) => node.id, source: (edge) => edge.source, target: (edge) => edge.target, sourceWeight: (edge) => edge.value || 1, targetWeight: (edge) => edge.value || 1, sortBy: null, // optional, id | weight | frequency | {function} }; export type ChordLayoutOptions = { weight?: boolean; y?: number; nodeWidthRatio?: number; // 节点的宽度比例,对应于极坐标系的厚度,(0, 1) nodePaddingRatio?: number; // 节点之间的间距的比例,[0, 1) id?(node: any): any; source?(edge: any): any; target?(edge: any): any; sourceWeight?(edge: any): number; targetWeight?(edge: any): number; sortBy?: 'id' | 'weight' | 'frequency' | null | ((a: any, b: any) => number); }; type OutputNode = { readonly id: number; readonly name: string; // readonly depth: number; readonly value: number; // 用于绘制 polygon x: number[]; y: number[]; }; type OutputLink = { readonly source: OutputNode; readonly target: OutputNode; readonly value: number; // 用于绘制 edge x?: number[]; y?: number[]; }; type ChordLayoutOutputData = { readonly nodes: OutputNode[]; readonly links: OutputLink[]; }; /** * 处理节点的value、edges * @param nodeById * @param edges * @param options */ function processGraph(nodeById, edges, options) { forIn(nodeById, (node, id) => { // in edges, out edges node.inEdges = edges.filter((edge) => `${options.target(edge)}` === `${id}`); node.outEdges = edges.filter((edge) => `${options.source(edge)}` === `${id}`); // frequency node.edges = node.outEdges.concat(node.inEdges); node.frequency = node.edges.length; // weight node.value = 0; node.inEdges.forEach((edge) => { node.value += options.targetWeight(edge); }); node.outEdges.forEach((edge) => { node.value += options.sourceWeight(edge); }); }); } /** * 节点排序 * @param nodes * @param options */ function sortNodes(nodes, options) { const sortMethods = { weight: (a, b) => b.value - a.value, frequency: (a, b) => b.frequency - a.frequency, id: (a, b) => `${options.id(a)}`.localeCompare(`${options.id(b)}`), }; let method = sortMethods[options.sortBy]; if (!method && isFunction(options.sortBy)) { method = options.sortBy; } if (method) { nodes.sort(method); } } function layoutNodes(nodes, options): OutputNode[] { const len = nodes.length; if (!len) { throw new TypeError("Invalid nodes: it's empty!"); } if (options.weight) { const nodePaddingRatio = options.nodePaddingRatio; if (nodePaddingRatio < 0 || nodePaddingRatio >= 1) { throw new TypeError('Invalid nodePaddingRatio: it must be in range [0, 1)!'); } const margin = nodePaddingRatio / (2 * len); const nodeWidthRatio = options.nodeWidthRatio; if (nodeWidthRatio <= 0 || nodeWidthRatio >= 1) { throw new TypeError('Invalid nodeWidthRatio: it must be in range (0, 1)!'); } let totalValue = 0; nodes.forEach((node) => { totalValue += node.value; }); nodes.forEach((node) => { node.weight = node.value / totalValue; node.width = node.weight * (1 - nodePaddingRatio); node.height = nodeWidthRatio; }); nodes.forEach((node, index) => { // x let deltaX = 0; for (let i = index - 1; i >= 0; i--) { deltaX += nodes[i].width + 2 * margin; } const minX = (node.minX = margin + deltaX); const maxX = (node.maxX = node.minX + node.width); const minY = (node.minY = options.y - nodeWidthRatio / 2); const maxY = (node.maxY = minY + nodeWidthRatio); node.x = [minX, maxX, maxX, minX]; node.y = [minY, minY, maxY, maxY]; /* points * 3---2 * | | * 0---1 */ // node.x = minX + 0.5 * node.width; // node.y = options.y; }); } else { const deltaX = 1 / len; nodes.forEach((node, index) => { node.x = (index + 0.5) * deltaX; node.y = options.y; }); } return nodes; } function locatingEdges(nodeById, edges, options): OutputLink[] { if (options.weight) { const valueById = {}; forIn(nodeById, (node, id) => { valueById[id] = node.value; }); edges.forEach((edge) => { const sId = options.source(edge); const tId = options.target(edge); const sNode = nodeById[sId]; const tNode = nodeById[tId]; if (sNode && tNode) { const sValue = valueById[sId]; const currentSValue = options.sourceWeight(edge); const sStart = sNode.minX + ((sNode.value - sValue) / sNode.value) * sNode.width; const sEnd = sStart + (currentSValue / sNode.value) * sNode.width; valueById[sId] -= currentSValue; const tValue = valueById[tId]; const currentTValue = options.targetWeight(edge); const tStart = tNode.minX + ((tNode.value - tValue) / tNode.value) * tNode.width; const tEnd = tStart + (currentTValue / tNode.value) * tNode.width; valueById[tId] -= currentTValue; const y = options.y; edge.x = [sStart, sEnd, tStart, tEnd]; edge.y = [y, y, y, y]; // 将edge的source与target的id换为 sourceNode与targetNode edge.source = sNode; edge.target = tNode; } }); } else { edges.forEach((edge) => { const sNode = nodeById[options.source(edge)]; const tNode = nodeById[options.target(edge)]; if (sNode && tNode) { edge.x = [sNode.x, tNode.x]; edge.y = [sNode.y, tNode.y]; // 将edge的source与target的id换为 sourceNode与targetNode edge.source = sNode; edge.target = tNode; } }); } return edges; } export function getDefaultOptions(options: ChordLayoutOptions): ChordLayoutOptions { return assign({}, DEFAULT_OPTIONS, options); } export function chordLayout( chordLayoutOptions: ChordLayoutOptions, chordLayoutInputData: NodeLinkData ): ChordLayoutOutputData { const options = getDefaultOptions(chordLayoutOptions); const nodeById = {}; const nodes = chordLayoutInputData.nodes; const links = chordLayoutInputData.links; nodes.forEach((node) => { const id = options.id(node); nodeById[id] = node; }); processGraph(nodeById, links, options); sortNodes(nodes, options); const outputNodes = layoutNodes(nodes, options); const outputLinks = locatingEdges(nodeById, links, options); return { nodes: outputNodes, links: outputLinks, }; }