update at 2026-02-14 11:43:49
This commit is contained in:
@@ -322,37 +322,48 @@ function applyDirection(links, direction) {
|
||||
/**
|
||||
* 根据标签位置模式返回 Canvas 文本绘制参数。
|
||||
*/
|
||||
function getCanvasLabelPlacement(layout, isSource, labelPositionMode) {
|
||||
const sourceInnerX = layout.leftX + layout.nodeWidth + 4;
|
||||
const sourceOuterX = Math.max(2, layout.leftX - 4);
|
||||
const targetInnerX = Math.max(2, layout.rightX - 4);
|
||||
const targetOuterX = layout.rightX + layout.nodeWidth + 4;
|
||||
function getCanvasLabelPlacement(nodeX, nodeWidth, isSource, isTarget, labelPositionMode) {
|
||||
const leftOuterX = Math.max(2, nodeX - 4);
|
||||
const rightOuterX = nodeX + nodeWidth + 4;
|
||||
|
||||
if (!isSource && !isTarget) {
|
||||
if (labelPositionMode === 'left') {
|
||||
return { x: leftOuterX, textAlign: 'right' };
|
||||
}
|
||||
return { x: rightOuterX, textAlign: 'left' };
|
||||
}
|
||||
|
||||
if (labelPositionMode === 'outer') {
|
||||
return isSource
|
||||
? { x: sourceOuterX, textAlign: 'right' }
|
||||
: { x: targetOuterX, textAlign: 'left' };
|
||||
? { x: leftOuterX, textAlign: 'right' }
|
||||
: { x: rightOuterX, textAlign: 'left' };
|
||||
}
|
||||
if (labelPositionMode === 'left') {
|
||||
return isSource
|
||||
? { x: sourceOuterX, textAlign: 'right' }
|
||||
: { x: targetInnerX, textAlign: 'right' };
|
||||
? { x: leftOuterX, textAlign: 'right' }
|
||||
: { x: leftOuterX, textAlign: 'right' };
|
||||
}
|
||||
if (labelPositionMode === 'right') {
|
||||
return isSource
|
||||
? { x: sourceInnerX, textAlign: 'left' }
|
||||
: { x: targetOuterX, textAlign: 'left' };
|
||||
? { x: rightOuterX, textAlign: 'left' }
|
||||
: { x: rightOuterX, textAlign: 'left' };
|
||||
}
|
||||
return isSource
|
||||
? { x: sourceInnerX, textAlign: 'left' }
|
||||
: { x: targetInnerX, textAlign: 'right' };
|
||||
? { x: rightOuterX, textAlign: 'left' }
|
||||
: { x: leftOuterX, textAlign: 'right' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签位置模式返回 SVG 文本锚点参数。
|
||||
*/
|
||||
function getSvgLabelPlacement(layout, isSource, labelPositionMode) {
|
||||
const canvasPlacement = getCanvasLabelPlacement(layout, isSource, labelPositionMode);
|
||||
function getSvgLabelPlacement(nodeX, nodeWidth, isSource, isTarget, labelPositionMode) {
|
||||
const canvasPlacement = getCanvasLabelPlacement(
|
||||
nodeX,
|
||||
nodeWidth,
|
||||
isSource,
|
||||
isTarget,
|
||||
labelPositionMode
|
||||
);
|
||||
return {
|
||||
x: canvasPlacement.x,
|
||||
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 targetAlignMode = (renderOptions && renderOptions.targetAlignMode) || 'between';
|
||||
const nodeWidth = 10;
|
||||
const leftX = padding;
|
||||
const rightX = Math.max(padding + nodeWidth + 80, width - padding - nodeWidth);
|
||||
const validLinks = links
|
||||
.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 nodeOrderMap = {};
|
||||
let nodeColorIndexCursor = 0;
|
||||
links.forEach((link) => {
|
||||
sourceValueMap[link.source] = (sourceValueMap[link.source] || 0) + Number(link.value || 0);
|
||||
targetValueMap[link.target] = (targetValueMap[link.target] || 0) + Number(link.value || 0);
|
||||
let nodeOrderCursor = 0;
|
||||
const incomingCountMap = {};
|
||||
const outgoingCountMap = {};
|
||||
const inValueMap = {};
|
||||
const outValueMap = {};
|
||||
const adjacencyMap = {};
|
||||
const indegreeMap = {};
|
||||
|
||||
validLinks.forEach((link) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(nodeColorIndexMap, link.source)) {
|
||||
nodeColorIndexMap[link.source] = nodeColorIndexCursor;
|
||||
nodeColorIndexCursor += 1;
|
||||
@@ -401,107 +425,226 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
||||
nodeColorIndexMap[link.target] = nodeColorIndexCursor;
|
||||
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 targetNames = Object.keys(targetValueMap);
|
||||
const totalValue = sourceNames.reduce((sum, name) => sum + sourceValueMap[name], 0);
|
||||
const nodeNames = Object.keys(nodeOrderMap).sort((a, b) => nodeOrderMap[a] - nodeOrderMap[b]);
|
||||
const nodeCount = nodeNames.length;
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
const sourceGapCount = Math.max(0, sourceNames.length - 1);
|
||||
const sourceContentHeight = Math.max(0, height - padding * 2 - sourceGapCount * nodeGap);
|
||||
const sourceUnitHeight = sourceContentHeight / totalValue;
|
||||
const sourceTotalNodeHeight = totalValue * sourceUnitHeight;
|
||||
const sourceSpanHeight = sourceTotalNodeHeight + sourceGapCount * nodeGap;
|
||||
|
||||
const sourcePos = {};
|
||||
let sourceCursorY = padding;
|
||||
sourceNames.forEach((name) => {
|
||||
const nodeHeight = sourceValueMap[name] * sourceUnitHeight;
|
||||
sourcePos[name] = { y: sourceCursorY, h: nodeHeight };
|
||||
sourceCursorY += nodeHeight + nodeGap;
|
||||
const columnNodeHeightMap = {};
|
||||
const columnGapMap = {};
|
||||
const columnBlockHeightMap = {};
|
||||
columns.forEach((names, depth) => {
|
||||
const totalNodeHeight = names.reduce((sum, name) => sum + (nodeValueMap[name] || 0) * unitHeight, 0);
|
||||
let columnGap = effectiveGap;
|
||||
if (depth === maxDepth && targetAlignMode === 'between' && names.length > 1) {
|
||||
const sourceNames = columns[0] || [];
|
||||
const sourceTotalNodeHeight = sourceNames.reduce(
|
||||
(sum, name) => sum + (nodeValueMap[name] || 0) * unitHeight,
|
||||
0
|
||||
);
|
||||
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 targetTotalValue = targetNames.reduce((sum, name) => sum + targetValueMap[name], 0);
|
||||
const targetUnitHeight = targetTotalValue > 0 ? sourceUnitHeight : 0;
|
||||
const targetTotalNodeHeight = targetTotalValue * targetUnitHeight;
|
||||
let targetGap = nodeGap;
|
||||
if (targetAlignMode === 'between' && targetNames.length > 1) {
|
||||
// 与 Web 端一致:两端对齐时,目标列整体高度与 source 列跨度一致。
|
||||
targetGap = (sourceSpanHeight - targetTotalNodeHeight) / (targetNames.length - 1);
|
||||
}
|
||||
targetGap = Math.max(0, targetGap);
|
||||
const sourceSpanHeight = columnBlockHeightMap[0] || 0;
|
||||
const leftX = padding;
|
||||
const rightX = Math.max(padding + nodeWidth + 80, width - padding - nodeWidth);
|
||||
const columnStep = maxDepth > 0 ? (rightX - leftX) / maxDepth : 0;
|
||||
|
||||
const targetBlockHeight = targetTotalNodeHeight + targetGapCount * targetGap;
|
||||
let targetStartY = padding;
|
||||
const nodePositionMap = {};
|
||||
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') {
|
||||
targetStartY = padding + Math.max(0, (sourceSpanHeight - targetBlockHeight) / 2);
|
||||
startY = padding + Math.max(0, (sourceSpanHeight - blockHeight) / 2);
|
||||
} else if (targetAlignMode === 'bottom') {
|
||||
targetStartY = padding + Math.max(0, sourceSpanHeight - targetBlockHeight);
|
||||
} else if (targetAlignMode === 'top') {
|
||||
targetStartY = padding;
|
||||
startY = padding + Math.max(0, sourceSpanHeight - blockHeight);
|
||||
}
|
||||
}
|
||||
|
||||
const targetPos = {};
|
||||
let targetCursorY = targetStartY;
|
||||
targetNames.forEach((name) => {
|
||||
const nodeHeight = targetValueMap[name] * targetUnitHeight;
|
||||
targetPos[name] = { y: targetCursorY, h: nodeHeight };
|
||||
targetCursorY += nodeHeight + targetGap;
|
||||
let cursorY = startY;
|
||||
const nodeX = leftX + columnStep * depth;
|
||||
names.forEach((name) => {
|
||||
const nodeHeight = (nodeValueMap[name] || 0) * unitHeight;
|
||||
const node = {
|
||||
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 targetOffset = {};
|
||||
sourceNames.forEach((name) => {
|
||||
sourceOffset[name] = 0;
|
||||
});
|
||||
targetNames.forEach((name) => {
|
||||
targetOffset[name] = 0;
|
||||
const sourceOffsetMap = {};
|
||||
const targetOffsetMap = {};
|
||||
nodeNames.forEach((name) => {
|
||||
sourceOffsetMap[name] = 0;
|
||||
targetOffsetMap[name] = 0;
|
||||
});
|
||||
|
||||
const linkSegments = [];
|
||||
links.forEach((link) => {
|
||||
const sourceNode = sourcePos[link.source];
|
||||
const targetNode = targetPos[link.target];
|
||||
validLinks.forEach((link) => {
|
||||
const sourceNode = nodePositionMap[link.source];
|
||||
const targetNode = nodePositionMap[link.target];
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkHeight = Number(link.value || 0) * sourceUnitHeight;
|
||||
const linkHeight = link.value * unitHeight;
|
||||
if (linkHeight <= 0) {
|
||||
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 endX = rightX;
|
||||
const controlX = (startX + endX) / 2;
|
||||
const sy = sourceNode.y + sourceOffsetMap[link.source] + linkHeight / 2;
|
||||
const ty = targetNode.y + targetOffsetMap[link.target] + linkHeight / 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({
|
||||
startX,
|
||||
endX,
|
||||
controlX,
|
||||
controlX1: startX + controlOffset,
|
||||
controlX2: endX - controlOffset,
|
||||
sy,
|
||||
ty,
|
||||
linkHeight,
|
||||
sourceIndex: Number.isFinite(nodeColorIndexMap[link.source]) ? nodeColorIndexMap[link.source] : 0
|
||||
sourceIndex: sourceNode.colorIndex
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
leftX,
|
||||
rightX,
|
||||
nodeWidth,
|
||||
sourceNames,
|
||||
targetNames,
|
||||
sourcePos,
|
||||
targetPos,
|
||||
maxDepth,
|
||||
nodes,
|
||||
nodeColorIndexMap,
|
||||
linkSegments
|
||||
};
|
||||
@@ -535,8 +678,8 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
|
||||
layout.linkSegments.forEach((segment) => {
|
||||
const pathData = `M ${formatSvgNumber(segment.startX)} ${formatSvgNumber(segment.sy)} C ${formatSvgNumber(
|
||||
segment.controlX
|
||||
)} ${formatSvgNumber(segment.sy)} ${formatSvgNumber(segment.controlX)} ${formatSvgNumber(segment.ty)} ${formatSvgNumber(
|
||||
segment.controlX1
|
||||
)} ${formatSvgNumber(segment.sy)} ${formatSvgNumber(segment.controlX2)} ${formatSvgNumber(segment.ty)} ${formatSvgNumber(
|
||||
segment.endX
|
||||
)} ${formatSvgNumber(segment.ty)}`;
|
||||
segments.push(
|
||||
@@ -549,51 +692,31 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
);
|
||||
});
|
||||
|
||||
layout.sourceNames.forEach((name, index) => {
|
||||
const node = layout.sourcePos[name];
|
||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||
? layout.nodeColorIndexMap[name]
|
||||
: index;
|
||||
layout.nodes.forEach((node) => {
|
||||
const isSource = node.depth === 0;
|
||||
const isTarget = node.depth === layout.maxDepth;
|
||||
const textY = getClampedLabelY(
|
||||
node.y,
|
||||
node.h,
|
||||
height,
|
||||
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(
|
||||
`<rect x="${formatSvgNumber(layout.leftX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
||||
`<rect x="${formatSvgNumber(node.x)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
||||
layout.nodeWidth
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(colorIndex, themeColors)}" />`
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(node.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>`
|
||||
);
|
||||
});
|
||||
|
||||
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>`
|
||||
}" dominant-baseline="middle">${escapeSvgText(node.name)}</text>`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1270,9 +1393,9 @@ Page({
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(segment.startX, segment.sy);
|
||||
ctx.bezierCurveTo(
|
||||
segment.controlX,
|
||||
segment.controlX1,
|
||||
segment.sy,
|
||||
segment.controlX,
|
||||
segment.controlX2,
|
||||
segment.ty,
|
||||
segment.endX,
|
||||
segment.ty
|
||||
@@ -1280,36 +1403,24 @@ Page({
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
layout.sourceNames.forEach((name, index) => {
|
||||
const node = layout.sourcePos[name];
|
||||
const labelPlacement = getCanvasLabelPlacement(layout, true, this.data.labelPositionMode);
|
||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||
? layout.nodeColorIndexMap[name]
|
||||
: index;
|
||||
layout.nodes.forEach((node) => {
|
||||
const isSource = node.depth === 0;
|
||||
const isTarget = node.depth === layout.maxDepth;
|
||||
const labelPlacement = getCanvasLabelPlacement(
|
||||
node.x,
|
||||
layout.nodeWidth,
|
||||
isSource,
|
||||
isTarget,
|
||||
this.data.labelPositionMode
|
||||
);
|
||||
const textY = getClampedLabelY(node.y, node.h, height, this.data.chartPadding);
|
||||
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
||||
ctx.fillRect(layout.leftX, node.y, layout.nodeWidth, node.h);
|
||||
ctx.setFillStyle(getNodeColor(node.colorIndex, themeColors));
|
||||
ctx.fillRect(node.x, 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);
|
||||
});
|
||||
|
||||
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.fillText(node.name, labelPlacement.x, textY);
|
||||
});
|
||||
|
||||
ctx.draw();
|
||||
|
||||
Reference in New Issue
Block a user