update at 2026-02-14 11:43:49
This commit is contained in:
@@ -322,37 +322,48 @@ function applyDirection(links, direction) {
|
|||||||
/**
|
/**
|
||||||
* 根据标签位置模式返回 Canvas 文本绘制参数。
|
* 根据标签位置模式返回 Canvas 文本绘制参数。
|
||||||
*/
|
*/
|
||||||
function getCanvasLabelPlacement(layout, isSource, labelPositionMode) {
|
function getCanvasLabelPlacement(nodeX, nodeWidth, isSource, isTarget, labelPositionMode) {
|
||||||
const sourceInnerX = layout.leftX + layout.nodeWidth + 4;
|
const leftOuterX = Math.max(2, nodeX - 4);
|
||||||
const sourceOuterX = Math.max(2, layout.leftX - 4);
|
const rightOuterX = nodeX + nodeWidth + 4;
|
||||||
const targetInnerX = Math.max(2, layout.rightX - 4);
|
|
||||||
const targetOuterX = layout.rightX + layout.nodeWidth + 4;
|
if (!isSource && !isTarget) {
|
||||||
|
if (labelPositionMode === 'left') {
|
||||||
|
return { x: leftOuterX, textAlign: 'right' };
|
||||||
|
}
|
||||||
|
return { x: rightOuterX, textAlign: 'left' };
|
||||||
|
}
|
||||||
|
|
||||||
if (labelPositionMode === 'outer') {
|
if (labelPositionMode === 'outer') {
|
||||||
return isSource
|
return isSource
|
||||||
? { x: sourceOuterX, textAlign: 'right' }
|
? { x: leftOuterX, textAlign: 'right' }
|
||||||
: { x: targetOuterX, textAlign: 'left' };
|
: { x: rightOuterX, textAlign: 'left' };
|
||||||
}
|
}
|
||||||
if (labelPositionMode === 'left') {
|
if (labelPositionMode === 'left') {
|
||||||
return isSource
|
return isSource
|
||||||
? { x: sourceOuterX, textAlign: 'right' }
|
? { x: leftOuterX, textAlign: 'right' }
|
||||||
: { x: targetInnerX, textAlign: 'right' };
|
: { x: leftOuterX, textAlign: 'right' };
|
||||||
}
|
}
|
||||||
if (labelPositionMode === 'right') {
|
if (labelPositionMode === 'right') {
|
||||||
return isSource
|
return isSource
|
||||||
? { x: sourceInnerX, textAlign: 'left' }
|
? { x: rightOuterX, textAlign: 'left' }
|
||||||
: { x: targetOuterX, textAlign: 'left' };
|
: { x: rightOuterX, textAlign: 'left' };
|
||||||
}
|
}
|
||||||
return isSource
|
return isSource
|
||||||
? { x: sourceInnerX, textAlign: 'left' }
|
? { x: rightOuterX, textAlign: 'left' }
|
||||||
: { x: targetInnerX, textAlign: 'right' };
|
: { x: leftOuterX, textAlign: 'right' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据标签位置模式返回 SVG 文本锚点参数。
|
* 根据标签位置模式返回 SVG 文本锚点参数。
|
||||||
*/
|
*/
|
||||||
function getSvgLabelPlacement(layout, isSource, labelPositionMode) {
|
function getSvgLabelPlacement(nodeX, nodeWidth, isSource, isTarget, labelPositionMode) {
|
||||||
const canvasPlacement = getCanvasLabelPlacement(layout, isSource, labelPositionMode);
|
const canvasPlacement = getCanvasLabelPlacement(
|
||||||
|
nodeX,
|
||||||
|
nodeWidth,
|
||||||
|
isSource,
|
||||||
|
isTarget,
|
||||||
|
labelPositionMode
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
x: canvasPlacement.x,
|
x: canvasPlacement.x,
|
||||||
textAnchor: canvasPlacement.textAlign === 'right' ? 'end' : 'start'
|
textAnchor: canvasPlacement.textAlign === 'right' ? 'end' : 'start'
|
||||||
@@ -383,16 +394,29 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
|||||||
const padding = Math.min(rawPadding, Math.max(0, height / 2 - 4));
|
const padding = Math.min(rawPadding, Math.max(0, height / 2 - 4));
|
||||||
const targetAlignMode = (renderOptions && renderOptions.targetAlignMode) || 'between';
|
const targetAlignMode = (renderOptions && renderOptions.targetAlignMode) || 'between';
|
||||||
const nodeWidth = 10;
|
const nodeWidth = 10;
|
||||||
const leftX = padding;
|
const validLinks = links
|
||||||
const rightX = Math.max(padding + nodeWidth + 80, width - padding - nodeWidth);
|
.map((link) => ({
|
||||||
|
source: String(link.source || ''),
|
||||||
|
target: String(link.target || ''),
|
||||||
|
value: Number(link.value || 0)
|
||||||
|
}))
|
||||||
|
.filter((link) => link.source && link.target && Number.isFinite(link.value) && link.value > 0);
|
||||||
|
if (validLinks.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const sourceValueMap = {};
|
|
||||||
const targetValueMap = {};
|
|
||||||
const nodeColorIndexMap = {};
|
const nodeColorIndexMap = {};
|
||||||
|
const nodeOrderMap = {};
|
||||||
let nodeColorIndexCursor = 0;
|
let nodeColorIndexCursor = 0;
|
||||||
links.forEach((link) => {
|
let nodeOrderCursor = 0;
|
||||||
sourceValueMap[link.source] = (sourceValueMap[link.source] || 0) + Number(link.value || 0);
|
const incomingCountMap = {};
|
||||||
targetValueMap[link.target] = (targetValueMap[link.target] || 0) + Number(link.value || 0);
|
const outgoingCountMap = {};
|
||||||
|
const inValueMap = {};
|
||||||
|
const outValueMap = {};
|
||||||
|
const adjacencyMap = {};
|
||||||
|
const indegreeMap = {};
|
||||||
|
|
||||||
|
validLinks.forEach((link) => {
|
||||||
if (!Object.prototype.hasOwnProperty.call(nodeColorIndexMap, link.source)) {
|
if (!Object.prototype.hasOwnProperty.call(nodeColorIndexMap, link.source)) {
|
||||||
nodeColorIndexMap[link.source] = nodeColorIndexCursor;
|
nodeColorIndexMap[link.source] = nodeColorIndexCursor;
|
||||||
nodeColorIndexCursor += 1;
|
nodeColorIndexCursor += 1;
|
||||||
@@ -401,107 +425,226 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
|||||||
nodeColorIndexMap[link.target] = nodeColorIndexCursor;
|
nodeColorIndexMap[link.target] = nodeColorIndexCursor;
|
||||||
nodeColorIndexCursor += 1;
|
nodeColorIndexCursor += 1;
|
||||||
}
|
}
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(nodeOrderMap, link.source)) {
|
||||||
|
nodeOrderMap[link.source] = nodeOrderCursor;
|
||||||
|
nodeOrderCursor += 1;
|
||||||
|
}
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(nodeOrderMap, link.target)) {
|
||||||
|
nodeOrderMap[link.target] = nodeOrderCursor;
|
||||||
|
nodeOrderCursor += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
outgoingCountMap[link.source] = (outgoingCountMap[link.source] || 0) + 1;
|
||||||
|
incomingCountMap[link.target] = (incomingCountMap[link.target] || 0) + 1;
|
||||||
|
outValueMap[link.source] = (outValueMap[link.source] || 0) + link.value;
|
||||||
|
inValueMap[link.target] = (inValueMap[link.target] || 0) + link.value;
|
||||||
|
|
||||||
|
if (!adjacencyMap[link.source]) {
|
||||||
|
adjacencyMap[link.source] = [];
|
||||||
|
}
|
||||||
|
adjacencyMap[link.source].push(link.target);
|
||||||
|
indegreeMap[link.target] = (indegreeMap[link.target] || 0) + 1;
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(indegreeMap, link.source)) {
|
||||||
|
indegreeMap[link.source] = indegreeMap[link.source] || 0;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const sourceNames = Object.keys(sourceValueMap);
|
const nodeNames = Object.keys(nodeOrderMap).sort((a, b) => nodeOrderMap[a] - nodeOrderMap[b]);
|
||||||
const targetNames = Object.keys(targetValueMap);
|
const nodeCount = nodeNames.length;
|
||||||
const totalValue = sourceNames.reduce((sum, name) => sum + sourceValueMap[name], 0);
|
const depthMap = {};
|
||||||
|
nodeNames.forEach((name) => {
|
||||||
|
depthMap[name] = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const indegreeWorkMap = {};
|
||||||
|
nodeNames.forEach((name) => {
|
||||||
|
indegreeWorkMap[name] = indegreeMap[name] || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const queue = nodeNames.filter((name) => indegreeWorkMap[name] === 0);
|
||||||
|
const visitedMap = {};
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift();
|
||||||
|
visitedMap[current] = true;
|
||||||
|
const nextTargets = adjacencyMap[current] || [];
|
||||||
|
nextTargets.forEach((next) => {
|
||||||
|
const nextDepth = Math.min(nodeCount - 1, (depthMap[current] || 0) + 1);
|
||||||
|
if (nextDepth > (depthMap[next] || 0)) {
|
||||||
|
depthMap[next] = nextDepth;
|
||||||
|
}
|
||||||
|
indegreeWorkMap[next] = (indegreeWorkMap[next] || 0) - 1;
|
||||||
|
if (indegreeWorkMap[next] === 0) {
|
||||||
|
queue.push(next);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图中存在环时,继续做有限轮次松弛,尽量拉开层级。
|
||||||
|
if (Object.keys(visitedMap).length < nodeCount) {
|
||||||
|
for (let round = 0; round < nodeCount; round += 1) {
|
||||||
|
let changed = false;
|
||||||
|
validLinks.forEach((link) => {
|
||||||
|
const relaxedDepth = Math.min(nodeCount - 1, (depthMap[link.source] || 0) + 1);
|
||||||
|
if (relaxedDepth > (depthMap[link.target] || 0)) {
|
||||||
|
depthMap[link.target] = relaxedDepth;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!changed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxDepth = nodeNames.reduce((max, name) => Math.max(max, depthMap[name] || 0), 0);
|
||||||
|
const columns = Array.from({ length: maxDepth + 1 }, () => []);
|
||||||
|
nodeNames.forEach((name) => {
|
||||||
|
const depth = depthMap[name] || 0;
|
||||||
|
columns[depth].push(name);
|
||||||
|
});
|
||||||
|
columns.forEach((names) => {
|
||||||
|
names.sort((a, b) => nodeOrderMap[a] - nodeOrderMap[b]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeValueMap = {};
|
||||||
|
nodeNames.forEach((name) => {
|
||||||
|
nodeValueMap[name] = Math.max(outValueMap[name] || 0, inValueMap[name] || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableHeight = Math.max(0, height - padding * 2);
|
||||||
|
const maxColumnNodeCount = columns.reduce((max, names) => Math.max(max, names.length), 0);
|
||||||
|
const effectiveGap =
|
||||||
|
maxColumnNodeCount > 1 ? Math.min(nodeGap, availableHeight / (maxColumnNodeCount - 1)) : nodeGap;
|
||||||
|
|
||||||
|
let unitHeight = Infinity;
|
||||||
|
columns.forEach((names) => {
|
||||||
|
if (names.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const totalValue = names.reduce((sum, name) => sum + (nodeValueMap[name] || 0), 0);
|
||||||
if (totalValue <= 0) {
|
if (totalValue <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gapCount = Math.max(0, names.length - 1);
|
||||||
|
const candidate = (availableHeight - gapCount * effectiveGap) / totalValue;
|
||||||
|
if (Number.isFinite(candidate) && candidate >= 0) {
|
||||||
|
unitHeight = Math.min(unitHeight, candidate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Number.isFinite(unitHeight) || unitHeight <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceGapCount = Math.max(0, sourceNames.length - 1);
|
const columnNodeHeightMap = {};
|
||||||
const sourceContentHeight = Math.max(0, height - padding * 2 - sourceGapCount * nodeGap);
|
const columnGapMap = {};
|
||||||
const sourceUnitHeight = sourceContentHeight / totalValue;
|
const columnBlockHeightMap = {};
|
||||||
const sourceTotalNodeHeight = totalValue * sourceUnitHeight;
|
columns.forEach((names, depth) => {
|
||||||
const sourceSpanHeight = sourceTotalNodeHeight + sourceGapCount * nodeGap;
|
const totalNodeHeight = names.reduce((sum, name) => sum + (nodeValueMap[name] || 0) * unitHeight, 0);
|
||||||
|
let columnGap = effectiveGap;
|
||||||
const sourcePos = {};
|
if (depth === maxDepth && targetAlignMode === 'between' && names.length > 1) {
|
||||||
let sourceCursorY = padding;
|
const sourceNames = columns[0] || [];
|
||||||
sourceNames.forEach((name) => {
|
const sourceTotalNodeHeight = sourceNames.reduce(
|
||||||
const nodeHeight = sourceValueMap[name] * sourceUnitHeight;
|
(sum, name) => sum + (nodeValueMap[name] || 0) * unitHeight,
|
||||||
sourcePos[name] = { y: sourceCursorY, h: nodeHeight };
|
0
|
||||||
sourceCursorY += nodeHeight + nodeGap;
|
);
|
||||||
|
const sourceSpanHeight =
|
||||||
|
sourceTotalNodeHeight + Math.max(0, sourceNames.length - 1) * effectiveGap;
|
||||||
|
columnGap = (sourceSpanHeight - totalNodeHeight) / (names.length - 1);
|
||||||
|
}
|
||||||
|
columnGap = Math.max(0, columnGap);
|
||||||
|
const blockHeight = totalNodeHeight + Math.max(0, names.length - 1) * columnGap;
|
||||||
|
columnNodeHeightMap[depth] = totalNodeHeight;
|
||||||
|
columnGapMap[depth] = columnGap;
|
||||||
|
columnBlockHeightMap[depth] = blockHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetGapCount = Math.max(0, targetNames.length - 1);
|
const sourceSpanHeight = columnBlockHeightMap[0] || 0;
|
||||||
const targetTotalValue = targetNames.reduce((sum, name) => sum + targetValueMap[name], 0);
|
const leftX = padding;
|
||||||
const targetUnitHeight = targetTotalValue > 0 ? sourceUnitHeight : 0;
|
const rightX = Math.max(padding + nodeWidth + 80, width - padding - nodeWidth);
|
||||||
const targetTotalNodeHeight = targetTotalValue * targetUnitHeight;
|
const columnStep = maxDepth > 0 ? (rightX - leftX) / maxDepth : 0;
|
||||||
let targetGap = nodeGap;
|
|
||||||
if (targetAlignMode === 'between' && targetNames.length > 1) {
|
|
||||||
// 与 Web 端一致:两端对齐时,目标列整体高度与 source 列跨度一致。
|
|
||||||
targetGap = (sourceSpanHeight - targetTotalNodeHeight) / (targetNames.length - 1);
|
|
||||||
}
|
|
||||||
targetGap = Math.max(0, targetGap);
|
|
||||||
|
|
||||||
const targetBlockHeight = targetTotalNodeHeight + targetGapCount * targetGap;
|
const nodePositionMap = {};
|
||||||
let targetStartY = padding;
|
const nodes = [];
|
||||||
|
columns.forEach((names, depth) => {
|
||||||
|
const blockHeight = columnBlockHeightMap[depth] || 0;
|
||||||
|
const columnGap = columnGapMap[depth] || 0;
|
||||||
|
let startY = padding;
|
||||||
|
if (depth === maxDepth) {
|
||||||
if (targetAlignMode === 'middle') {
|
if (targetAlignMode === 'middle') {
|
||||||
targetStartY = padding + Math.max(0, (sourceSpanHeight - targetBlockHeight) / 2);
|
startY = padding + Math.max(0, (sourceSpanHeight - blockHeight) / 2);
|
||||||
} else if (targetAlignMode === 'bottom') {
|
} else if (targetAlignMode === 'bottom') {
|
||||||
targetStartY = padding + Math.max(0, sourceSpanHeight - targetBlockHeight);
|
startY = padding + Math.max(0, sourceSpanHeight - blockHeight);
|
||||||
} else if (targetAlignMode === 'top') {
|
}
|
||||||
targetStartY = padding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetPos = {};
|
let cursorY = startY;
|
||||||
let targetCursorY = targetStartY;
|
const nodeX = leftX + columnStep * depth;
|
||||||
targetNames.forEach((name) => {
|
names.forEach((name) => {
|
||||||
const nodeHeight = targetValueMap[name] * targetUnitHeight;
|
const nodeHeight = (nodeValueMap[name] || 0) * unitHeight;
|
||||||
targetPos[name] = { y: targetCursorY, h: nodeHeight };
|
const node = {
|
||||||
targetCursorY += nodeHeight + targetGap;
|
name,
|
||||||
|
depth,
|
||||||
|
x: nodeX,
|
||||||
|
y: cursorY,
|
||||||
|
h: nodeHeight,
|
||||||
|
colorIndex: Number.isFinite(nodeColorIndexMap[name]) ? nodeColorIndexMap[name] : 0
|
||||||
|
};
|
||||||
|
nodes.push(node);
|
||||||
|
nodePositionMap[name] = node;
|
||||||
|
cursorY += nodeHeight + columnGap;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const sourceOffset = {};
|
const sourceOffsetMap = {};
|
||||||
const targetOffset = {};
|
const targetOffsetMap = {};
|
||||||
sourceNames.forEach((name) => {
|
nodeNames.forEach((name) => {
|
||||||
sourceOffset[name] = 0;
|
sourceOffsetMap[name] = 0;
|
||||||
});
|
targetOffsetMap[name] = 0;
|
||||||
targetNames.forEach((name) => {
|
|
||||||
targetOffset[name] = 0;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkSegments = [];
|
const linkSegments = [];
|
||||||
links.forEach((link) => {
|
validLinks.forEach((link) => {
|
||||||
const sourceNode = sourcePos[link.source];
|
const sourceNode = nodePositionMap[link.source];
|
||||||
const targetNode = targetPos[link.target];
|
const targetNode = nodePositionMap[link.target];
|
||||||
if (!sourceNode || !targetNode) {
|
if (!sourceNode || !targetNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkHeight = Number(link.value || 0) * sourceUnitHeight;
|
const linkHeight = link.value * unitHeight;
|
||||||
if (linkHeight <= 0) {
|
if (linkHeight <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sy = sourceNode.y + sourceOffset[link.source] + linkHeight / 2;
|
|
||||||
const ty = targetNode.y + targetOffset[link.target] + linkHeight / 2;
|
|
||||||
sourceOffset[link.source] += linkHeight;
|
|
||||||
targetOffset[link.target] += linkHeight;
|
|
||||||
|
|
||||||
const startX = leftX + nodeWidth;
|
const sy = sourceNode.y + sourceOffsetMap[link.source] + linkHeight / 2;
|
||||||
const endX = rightX;
|
const ty = targetNode.y + targetOffsetMap[link.target] + linkHeight / 2;
|
||||||
const controlX = (startX + endX) / 2;
|
sourceOffsetMap[link.source] += linkHeight;
|
||||||
|
targetOffsetMap[link.target] += linkHeight;
|
||||||
|
|
||||||
|
const startX = sourceNode.x + nodeWidth;
|
||||||
|
const endX = targetNode.x;
|
||||||
|
const distance = endX - startX;
|
||||||
|
if (distance <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const controlOffset = distance * 0.45;
|
||||||
|
|
||||||
linkSegments.push({
|
linkSegments.push({
|
||||||
startX,
|
startX,
|
||||||
endX,
|
endX,
|
||||||
controlX,
|
controlX1: startX + controlOffset,
|
||||||
|
controlX2: endX - controlOffset,
|
||||||
sy,
|
sy,
|
||||||
ty,
|
ty,
|
||||||
linkHeight,
|
linkHeight,
|
||||||
sourceIndex: Number.isFinite(nodeColorIndexMap[link.source]) ? nodeColorIndexMap[link.source] : 0
|
sourceIndex: sourceNode.colorIndex
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
leftX,
|
|
||||||
rightX,
|
|
||||||
nodeWidth,
|
nodeWidth,
|
||||||
sourceNames,
|
maxDepth,
|
||||||
targetNames,
|
nodes,
|
||||||
sourcePos,
|
|
||||||
targetPos,
|
|
||||||
nodeColorIndexMap,
|
nodeColorIndexMap,
|
||||||
linkSegments
|
linkSegments
|
||||||
};
|
};
|
||||||
@@ -535,8 +678,8 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
|||||||
|
|
||||||
layout.linkSegments.forEach((segment) => {
|
layout.linkSegments.forEach((segment) => {
|
||||||
const pathData = `M ${formatSvgNumber(segment.startX)} ${formatSvgNumber(segment.sy)} C ${formatSvgNumber(
|
const pathData = `M ${formatSvgNumber(segment.startX)} ${formatSvgNumber(segment.sy)} C ${formatSvgNumber(
|
||||||
segment.controlX
|
segment.controlX1
|
||||||
)} ${formatSvgNumber(segment.sy)} ${formatSvgNumber(segment.controlX)} ${formatSvgNumber(segment.ty)} ${formatSvgNumber(
|
)} ${formatSvgNumber(segment.sy)} ${formatSvgNumber(segment.controlX2)} ${formatSvgNumber(segment.ty)} ${formatSvgNumber(
|
||||||
segment.endX
|
segment.endX
|
||||||
)} ${formatSvgNumber(segment.ty)}`;
|
)} ${formatSvgNumber(segment.ty)}`;
|
||||||
segments.push(
|
segments.push(
|
||||||
@@ -549,51 +692,31 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
layout.sourceNames.forEach((name, index) => {
|
layout.nodes.forEach((node) => {
|
||||||
const node = layout.sourcePos[name];
|
const isSource = node.depth === 0;
|
||||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
const isTarget = node.depth === layout.maxDepth;
|
||||||
? layout.nodeColorIndexMap[name]
|
|
||||||
: index;
|
|
||||||
const textY = getClampedLabelY(
|
const textY = getClampedLabelY(
|
||||||
node.y,
|
node.y,
|
||||||
node.h,
|
node.h,
|
||||||
height,
|
height,
|
||||||
clampNumber(renderOptions && renderOptions.chartPadding, 0, 120, 16)
|
clampNumber(renderOptions && renderOptions.chartPadding, 0, 120, 16)
|
||||||
);
|
);
|
||||||
const textPlacement = getSvgLabelPlacement(layout, true, labelPositionMode);
|
const textPlacement = getSvgLabelPlacement(
|
||||||
|
node.x,
|
||||||
|
layout.nodeWidth,
|
||||||
|
isSource,
|
||||||
|
isTarget,
|
||||||
|
labelPositionMode
|
||||||
|
);
|
||||||
segments.push(
|
segments.push(
|
||||||
`<rect x="${formatSvgNumber(layout.leftX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
`<rect x="${formatSvgNumber(node.x)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
||||||
layout.nodeWidth
|
layout.nodeWidth
|
||||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(colorIndex, themeColors)}" />`
|
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(node.colorIndex, themeColors)}" />`
|
||||||
);
|
);
|
||||||
segments.push(
|
segments.push(
|
||||||
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
|
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
|
||||||
textPlacement.textAnchor
|
textPlacement.textAnchor
|
||||||
}" dominant-baseline="middle">${escapeSvgText(name)}</text>`
|
}" dominant-baseline="middle">${escapeSvgText(node.name)}</text>`
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
layout.targetNames.forEach((name, index) => {
|
|
||||||
const node = layout.targetPos[name];
|
|
||||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
|
||||||
? layout.nodeColorIndexMap[name]
|
|
||||||
: index;
|
|
||||||
const textY = getClampedLabelY(
|
|
||||||
node.y,
|
|
||||||
node.h,
|
|
||||||
height,
|
|
||||||
clampNumber(renderOptions && renderOptions.chartPadding, 0, 120, 16)
|
|
||||||
);
|
|
||||||
const textPlacement = getSvgLabelPlacement(layout, false, labelPositionMode);
|
|
||||||
segments.push(
|
|
||||||
`<rect x="${formatSvgNumber(layout.rightX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
|
||||||
layout.nodeWidth
|
|
||||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(colorIndex, themeColors)}" />`
|
|
||||||
);
|
|
||||||
segments.push(
|
|
||||||
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
|
|
||||||
textPlacement.textAnchor
|
|
||||||
}" dominant-baseline="middle">${escapeSvgText(name)}</text>`
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1270,9 +1393,9 @@ Page({
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(segment.startX, segment.sy);
|
ctx.moveTo(segment.startX, segment.sy);
|
||||||
ctx.bezierCurveTo(
|
ctx.bezierCurveTo(
|
||||||
segment.controlX,
|
segment.controlX1,
|
||||||
segment.sy,
|
segment.sy,
|
||||||
segment.controlX,
|
segment.controlX2,
|
||||||
segment.ty,
|
segment.ty,
|
||||||
segment.endX,
|
segment.endX,
|
||||||
segment.ty
|
segment.ty
|
||||||
@@ -1280,36 +1403,24 @@ Page({
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
|
|
||||||
layout.sourceNames.forEach((name, index) => {
|
layout.nodes.forEach((node) => {
|
||||||
const node = layout.sourcePos[name];
|
const isSource = node.depth === 0;
|
||||||
const labelPlacement = getCanvasLabelPlacement(layout, true, this.data.labelPositionMode);
|
const isTarget = node.depth === layout.maxDepth;
|
||||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
const labelPlacement = getCanvasLabelPlacement(
|
||||||
? layout.nodeColorIndexMap[name]
|
node.x,
|
||||||
: index;
|
layout.nodeWidth,
|
||||||
|
isSource,
|
||||||
|
isTarget,
|
||||||
|
this.data.labelPositionMode
|
||||||
|
);
|
||||||
const textY = getClampedLabelY(node.y, node.h, height, this.data.chartPadding);
|
const textY = getClampedLabelY(node.y, node.h, height, this.data.chartPadding);
|
||||||
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
ctx.setFillStyle(getNodeColor(node.colorIndex, themeColors));
|
||||||
ctx.fillRect(layout.leftX, node.y, layout.nodeWidth, node.h);
|
ctx.fillRect(node.x, node.y, layout.nodeWidth, node.h);
|
||||||
ctx.setFillStyle('#4e5969');
|
ctx.setFillStyle('#4e5969');
|
||||||
ctx.setFontSize(10);
|
ctx.setFontSize(10);
|
||||||
ctx.setTextAlign(labelPlacement.textAlign);
|
ctx.setTextAlign(labelPlacement.textAlign);
|
||||||
ctx.setTextBaseline('middle');
|
ctx.setTextBaseline('middle');
|
||||||
ctx.fillText(name, labelPlacement.x, textY);
|
ctx.fillText(node.name, labelPlacement.x, textY);
|
||||||
});
|
|
||||||
|
|
||||||
layout.targetNames.forEach((name, index) => {
|
|
||||||
const node = layout.targetPos[name];
|
|
||||||
const labelPlacement = getCanvasLabelPlacement(layout, false, this.data.labelPositionMode);
|
|
||||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
|
||||||
? layout.nodeColorIndexMap[name]
|
|
||||||
: index;
|
|
||||||
const textY = getClampedLabelY(node.y, node.h, height, this.data.chartPadding);
|
|
||||||
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
|
||||||
ctx.fillRect(layout.rightX, node.y, layout.nodeWidth, node.h);
|
|
||||||
ctx.setFillStyle('#4e5969');
|
|
||||||
ctx.setFontSize(10);
|
|
||||||
ctx.setTextAlign(labelPlacement.textAlign);
|
|
||||||
ctx.setTextBaseline('middle');
|
|
||||||
ctx.fillText(name, labelPlacement.x, textY);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.draw();
|
ctx.draw();
|
||||||
|
|||||||
Reference in New Issue
Block a user