/* * for Arc Diagram (edges without weight) / Chord Diagram (edges with source and target weight) * graph data required (nodes, edges) */ import { assign, forIn, isArray, values, isFunction } from '@antv/util'; import { DataSet } from '../../data-set'; import { View } from '../../view'; const DEFAULT_OPTIONS = { y: 0, thickness: 0.05, // thickness of the node, (0, 1) weight: false, marginRatio: 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 interface Options { y?: number; thickness?: number; weight?: boolean; marginRatio?: number; id?(node: any): any; source?(edge: any): any; target?(edge: any): any; sourceWeight?(edge: any): number; targetWeight?(edge: any): number; sortBy?: 'id' | 'weigth' | 'frequency' | null | ((a: any, b: any) => number); } function _nodesFromEdges(edges, options, map = {}) { edges.forEach((edge) => { const sId = options.edgeSource(edge); const tId = options.edgeTarget(edge); if (!map[sId]) { map[sId] = { id: sId, }; } if (!map[tId]) { map[tId] = { id: tId, }; } }); return values(map); } 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); }); }); } 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) { const len = nodes.length; if (!len) { throw new TypeError("Invalid nodes: it's empty!"); } if (options.weight) { const marginRatio = options.marginRatio; if (marginRatio < 0 || marginRatio >= 1) { throw new TypeError('Invalid marginRatio: it must be in range [0, 1)!'); } const margin = marginRatio / (2 * len); const thickness = options.thickness; if (thickness <= 0 || thickness >= 1) { throw new TypeError('Invalid thickness: 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 - marginRatio); node.height = thickness; }); 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 - thickness / 2); const maxY = (node.maxY = minY + thickness); 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; }); } } function _locatingEdges(nodeById, edges, options) { 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]; } }); } 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]; } }); } } function transform(dv: View, options) { options = assign({}, DEFAULT_OPTIONS, options); const nodeById = {}; let nodes = dv.nodes; const edges = dv.edges; if (!isArray(nodes) || nodes.length === 0) { nodes = _nodesFromEdges(edges, options, nodeById); } nodes.forEach((node) => { const id = options.id(node); nodeById[id] = node; }); _processGraph(nodeById, edges, options); _sortNodes(nodes, options); _layoutNodes(nodes, options); _locatingEdges(nodeById, edges, options); dv.nodes = nodes; dv.edges = edges; } DataSet.registerTransform('diagram.arc', transform); DataSet.registerTransform('arc', transform);