update at 2026-02-14 11:37:46

This commit is contained in:
douboer@gmail.com
2026-02-14 11:37:46 +08:00
parent d9e89f080e
commit 44d4afd037
2 changed files with 37 additions and 10 deletions

View File

@@ -359,6 +359,16 @@ function getSvgLabelPlacement(layout, isSource, labelPositionMode) {
};
}
/**
* 标签纵坐标限制在画布安全区内,避免上下溢出。
*/
function getClampedLabelY(nodeY, nodeHeight, canvasHeight, chartPadding) {
const rawY = nodeY + getNodeLabelCenterY(nodeHeight);
const safeTop = Math.max(0, chartPadding + 6);
const safeBottom = Math.max(safeTop, canvasHeight - chartPadding - 6);
return Math.min(safeBottom, Math.max(safeTop, rawY));
}
/**
* 统一生成桑基图布局数据,供 canvas 渲染与 SVG 导出共用。
*/
@@ -368,7 +378,9 @@ function buildSankeyLayout(links, width, height, renderOptions) {
}
const nodeGap = clampNumber(renderOptions && renderOptions.nodeGap, 0, 60, 8);
const padding = clampNumber(renderOptions && renderOptions.chartPadding, 0, 120, 16);
const rawPadding = clampNumber(renderOptions && renderOptions.chartPadding, 0, 120, 16);
// 纵向限制padding 不能超过可视高度的一半,避免节点整体被挤出画布。
const padding = Math.min(rawPadding, Math.max(0, height / 2 - 4));
const targetAlignMode = (renderOptions && renderOptions.targetAlignMode) || 'between';
const nodeWidth = 10;
const leftX = padding;
@@ -399,7 +411,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
}
const sourceGapCount = Math.max(0, sourceNames.length - 1);
const sourceContentHeight = Math.max(10, height - padding * 2 - sourceGapCount * nodeGap);
const sourceContentHeight = Math.max(0, height - padding * 2 - sourceGapCount * nodeGap);
const sourceUnitHeight = sourceContentHeight / totalValue;
const sourceTotalNodeHeight = totalValue * sourceUnitHeight;
const sourceSpanHeight = sourceTotalNodeHeight + sourceGapCount * nodeGap;
@@ -407,7 +419,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
const sourcePos = {};
let sourceCursorY = padding;
sourceNames.forEach((name) => {
const nodeHeight = Math.max(2, sourceValueMap[name] * sourceUnitHeight);
const nodeHeight = sourceValueMap[name] * sourceUnitHeight;
sourcePos[name] = { y: sourceCursorY, h: nodeHeight };
sourceCursorY += nodeHeight + nodeGap;
});
@@ -436,7 +448,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
const targetPos = {};
let targetCursorY = targetStartY;
targetNames.forEach((name) => {
const nodeHeight = Math.max(2, targetValueMap[name] * targetUnitHeight);
const nodeHeight = targetValueMap[name] * targetUnitHeight;
targetPos[name] = { y: targetCursorY, h: nodeHeight };
targetCursorY += nodeHeight + targetGap;
});
@@ -458,7 +470,10 @@ function buildSankeyLayout(links, width, height, renderOptions) {
return;
}
const linkHeight = Math.max(1, Number(link.value || 0) * sourceUnitHeight);
const linkHeight = Number(link.value || 0) * sourceUnitHeight;
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;
@@ -539,7 +554,12 @@ function buildSankeySvgText(links, width, height, renderOptions) {
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
? layout.nodeColorIndexMap[name]
: index;
const textY = node.y + getNodeLabelCenterY(node.h);
const textY = getClampedLabelY(
node.y,
node.h,
height,
clampNumber(renderOptions && renderOptions.chartPadding, 0, 120, 16)
);
const textPlacement = getSvgLabelPlacement(layout, true, labelPositionMode);
segments.push(
`<rect x="${formatSvgNumber(layout.leftX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
@@ -558,7 +578,12 @@ function buildSankeySvgText(links, width, height, renderOptions) {
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
? layout.nodeColorIndexMap[name]
: index;
const textY = node.y + getNodeLabelCenterY(node.h);
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(
@@ -1261,13 +1286,14 @@ Page({
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.leftX, node.y, layout.nodeWidth, node.h);
ctx.setFillStyle('#4e5969');
ctx.setFontSize(10);
ctx.setTextAlign(labelPlacement.textAlign);
ctx.setTextBaseline('middle');
ctx.fillText(name, labelPlacement.x, node.y + getNodeLabelCenterY(node.h));
ctx.fillText(name, labelPlacement.x, textY);
});
layout.targetNames.forEach((name, index) => {
@@ -1276,13 +1302,14 @@ Page({
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, node.y + getNodeLabelCenterY(node.h));
ctx.fillText(name, labelPlacement.x, textY);
});
ctx.draw();

View File

@@ -234,7 +234,7 @@
.preview-canvas {
margin-top: 3px;
width: 100%;
height: auto;
height: 0;
flex: 1;
min-height: 120px;
border-radius: 4px;