UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

389 lines (339 loc) 12.7 kB
import { deepExtend } from '../common'; const max = (array, mapFn) => Math.max.apply(null, array.map(mapFn)); const min = (array, mapFn) => Math.min.apply(null, array.map(mapFn)); const sum = (array, mapFn) => array.map(mapFn).reduce((acc, curr) => (acc + curr), 0); const sortAsc = (a, b) => (a.y0 === b.y0 ? a.index - b.index : a.y0 + a.y1 - b.y0 - b.y1); const sortSource = (a, b) => sortAsc(a.source, b.source); const sortTarget = (a, b) => sortAsc(a.target, b.target); const value = (node) => node.value; function sortLinks(nodes) { nodes.forEach(node => { node.targetLinks.forEach(link => { link.source.sourceLinks.sort(sortTarget); }); node.sourceLinks.forEach(link => { link.target.targetLinks.sort(sortSource); }); }); } const calcLayer = (node, maxDepth) => { if (node.align === 'left') { return node.depth; } if (node.align === 'right') { return maxDepth - node.height; } return node.sourceLinks.length ? node.depth : maxDepth; }; class Sankey { constructor(options) { const { offset = {}, align } = options.nodesOptions; this.data = { nodes: options.nodes.map((node) => deepExtend({}, { offset, align }, node)), links: options.links.map((link) => deepExtend({}, link)) }; this.width = options.width; this.height = options.height; this.offsetX = options.offsetX || 0; this.offsetY = options.offsetY || 0; this.nodeWidth = options.nodesOptions.width; this.nodePadding = options.nodesOptions.padding; this.reverse = options.reverse; this.targetColumnIndex = options.targetColumnIndex; this.loops = options.loops; this.autoLayout = options.autoLayout; } calculate() { const { nodes, links } = this.data; this.connectLinksToNodes(nodes, links); this.calculateNodeValues(nodes); const circularLinks = this.calculateNodeHeights(nodes); if (circularLinks) { return { nodes: [], links: [], columns: [], circularLinks }; } this.calculateNodeDepths(nodes); const columns = this.calculateNodeColumns(nodes); this.calculateNodeBreadths(columns); this.applyNodesOffset(nodes); this.calculateLinkBreadths(nodes); return Object.assign({}, this.data, {columns}); } connectLinksToNodes(nodes, links) { const nodesMap = new Map(); nodes.forEach((node, i) => { node.index = i; node.sourceLinks = []; node.targetLinks = []; node.id = node.id !== undefined ? node.id : node.label.text; nodesMap.set(node.id, node); }); links.forEach((link) => { link.source = nodesMap.get(link.sourceId); link.target = nodesMap.get(link.targetId); link.source.sourceLinks.push(link); link.target.targetLinks.push(link); }); } calculateNodeValues(nodes) { nodes.forEach((node) => { node.value = Math.max( sum(node.sourceLinks, value), sum(node.targetLinks, value) ); }); } calculateNodeDepths(nodes) { let current = new Set(nodes); let next = new Set(); let currDepth = 0; while (current.size) { const currentNodes = Array.from(current); for (let n = 0; n < currentNodes.length; n++) { const node = currentNodes[n]; node.depth = currDepth; for (let l = 0; l < node.sourceLinks.length; l++) { const link = node.sourceLinks[l]; next.add(link.target); } } currDepth++; current = next; next = new Set(); } } calculateNodeHeights(nodes) { const nodesLength = nodes.length; let current = new Set(nodes); let next = new Set; let currentHeight = 0; const eachNode = (node) => { node.height = currentHeight; node.targetLinks.forEach((link) => { next.add(link.source); }); }; while (current.size) { current.forEach(eachNode); currentHeight++; if (currentHeight > nodesLength) { return true; } current = next; next = new Set; } return false; } calculateNodeColumns(nodes) { const maxDepth = max(nodes, (d) => d.depth); const columnWidth = (this.width - this.offsetX - this.nodeWidth) / maxDepth; const columns = new Array(maxDepth + 1); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const layer = Math.max(0, Math.min(maxDepth, calcLayer(node, maxDepth))); node.x0 = this.offsetX + layer * columnWidth; node.x1 = node.x0 + this.nodeWidth; node.layer = layer; columns[layer] = columns[layer] || []; columns[layer].push(node); } return columns; } calculateNodeBreadths(columns) { const kSize = min(columns, (c) => (this.height - this.offsetY - (c.length - 1) * this.nodePadding) / sum(c, value)); columns.forEach(nodes => { let y = this.offsetY; nodes.forEach((node) => { node.y0 = y; node.y1 = y + node.value * kSize; y = node.y1 + this.nodePadding; node.sourceLinks.forEach((link) => { link.width = link.value * kSize; }); }); y = (this.height - y + this.nodePadding) / (nodes.length + 1); nodes.forEach((node, i) => { node.y0 += y * (i + 1); node.y1 += y * (i + 1); }); }); if (this.autoLayout !== false) { const loops = this.loops !== undefined ? this.loops : columns.length - 1; const targetColumnIndex = this.targetColumnIndex || 1; for (let i = 0; i < loops; i++) { if (!this.reverse) { this.uncurlLinksToLeft(columns, targetColumnIndex); this.uncurlLinksToRight(columns, targetColumnIndex); } else { this.uncurlLinksToRight(columns, targetColumnIndex); this.uncurlLinksToLeft(columns, targetColumnIndex); } } } columns.forEach(sortLinks); } applyNodesOffset(nodes) { nodes.forEach((node) => { const offsetX = (node.offset ? node.offset.left : 0) || 0; const offsetY = (node.offset ? node.offset.top : 0) || 0; node.x0 += offsetX; node.x1 += offsetX; node.y0 += offsetY; node.y1 += offsetY; }); } calculateLinkBreadths(nodes) { nodes.forEach((node) => { const { sourceLinks, targetLinks } = node; let y = node.y0; let y1 = y; sourceLinks.forEach((link) => { link.x0 = link.source.x1; link.y0 = y + link.width / 2; y += link.width; }); targetLinks.forEach((link) => { link.x1 = link.target.x0; link.y1 = y1 + link.width / 2; y1 += link.width; }); }); } uncurlLinksToRight(columns, targetColumnIndex) { const n = columns.length; for (let i = targetColumnIndex; i < n; i++) { const column = columns[i]; column.forEach((target) => { let y = 0; let sum = 0; target.targetLinks.forEach((link) => { let kValue = link.value * (target.layer - link.source.layer); y += this.targetTopPos(link.source, target) * kValue; sum += kValue; }); let dy = y === 0 ? 0 : (y / sum - target.y0); target.y0 += dy; target.y1 += dy; sortLinks([target]); }); column.sort(sortAsc); this.arrangeNodesVertically(column); } } uncurlLinksToLeft(columns, targetColumnIndex) { const l = columns.length; const startIndex = l - 1 - targetColumnIndex; for (let i = startIndex; i >= 0; i--) { const column = columns[i]; for (let j = 0; j < column.length; j++) { const source = column[j]; let y = 0; let sum = 0; source.sourceLinks.forEach((link) => { let kValue = link.value * (link.target.layer - source.layer); y += this.sourceTopPos(source, link.target) * kValue; sum += kValue; }); let dy = y === 0 ? 0 : (y / sum - source.y0); source.y0 += dy; source.y1 += dy; sortLinks([source]); } column.sort(sortAsc); this.arrangeNodesVertically(column); } } arrangeNodesVertically(nodes) { const startIndex = 0; const endIndex = nodes.length - 1; this.arrangeUp(nodes, this.height, endIndex); this.arrangeDown(nodes, this.offsetY, startIndex); } arrangeDown(nodes, yPos, index) { let currentY = yPos; for (let i = index; i < nodes.length; i++) { const node = nodes[i]; const dy = Math.max(0, currentY - node.y0); node.y0 += dy; node.y1 += dy; currentY = node.y1 + this.nodePadding; } } arrangeUp(nodes, yPos, index) { let currentY = yPos; for (let i = index; i >= 0; --i) { const node = nodes[i]; const dy = Math.max(0, node.y1 - currentY); node.y0 -= dy; node.y1 -= dy; currentY = node.y0 - this.nodePadding; } } sourceTopPos(source, target) { let y = target.y0 - ((target.targetLinks.length - 1) * this.nodePadding) / 2; for (let i = 0; i < target.targetLinks.length; i++) { const link = target.targetLinks[i]; if (link.source === source) { break; } y += link.width + this.nodePadding; } for (let i = 0; i < source.sourceLinks.length; i++) { const link = source.sourceLinks[i]; if (link.target === target) { break; } y -= link.width; } return y; } targetTopPos(source, target) { let y = source.y0 - ((source.sourceLinks.length - 1) * this.nodePadding) / 2; for (let i = 0; i < source.sourceLinks.length; i++) { const link = source.sourceLinks[i]; if (link.target === target) { break; } y += link.width + this.nodePadding; } for (let i = 0; i < target.targetLinks.length; i++) { const link = target.targetLinks[i]; if (link.source === source) { break; } y -= link.width; } return y; } } export const calculateSankey = (options) => new Sankey(options).calculate(); export const crossesValue = (links) => { let value = 0; const linksLength = links.length; for (let i = 0; i < linksLength; i++) { const link = links[i]; for (let lNext = i + 1; lNext < linksLength; lNext++) { const nextLink = links[lNext]; if (intersect(link, nextLink)) { value += Math.min(link.value, nextLink.value); } } } return value; }; function rotationDirection(p1x, p1y, p2x, p2y, p3x, p3y) { const expression1 = (p3y - p1y) * (p2x - p1x); const expression2 = (p2y - p1y) * (p3x - p1x); if (expression1 > expression2) { return 1; } else if (expression1 === expression2) { return 0; } return -1; } function intersect(link1, link2) { const f1 = rotationDirection(link1.x0, link1.y0, link1.x1, link1.y1, link2.x1, link2.y1); const f2 = rotationDirection(link1.x0, link1.y0, link1.x1, link1.y1, link2.x0, link2.y0); const f3 = rotationDirection(link1.x0, link1.y0, link2.x0, link2.y0, link2.x1, link2.y1); const f4 = rotationDirection(link1.x1, link1.y1, link2.x0, link2.y0, link2.x1, link2.y1); return f1 !== f2 && f3 !== f4; }