update at 2026-02-14 11:37:46
This commit is contained in:
@@ -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 导出共用。
|
* 统一生成桑基图布局数据,供 canvas 渲染与 SVG 导出共用。
|
||||||
*/
|
*/
|
||||||
@@ -368,7 +378,9 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nodeGap = clampNumber(renderOptions && renderOptions.nodeGap, 0, 60, 8);
|
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 targetAlignMode = (renderOptions && renderOptions.targetAlignMode) || 'between';
|
||||||
const nodeWidth = 10;
|
const nodeWidth = 10;
|
||||||
const leftX = padding;
|
const leftX = padding;
|
||||||
@@ -399,7 +411,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sourceGapCount = Math.max(0, sourceNames.length - 1);
|
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 sourceUnitHeight = sourceContentHeight / totalValue;
|
||||||
const sourceTotalNodeHeight = totalValue * sourceUnitHeight;
|
const sourceTotalNodeHeight = totalValue * sourceUnitHeight;
|
||||||
const sourceSpanHeight = sourceTotalNodeHeight + sourceGapCount * nodeGap;
|
const sourceSpanHeight = sourceTotalNodeHeight + sourceGapCount * nodeGap;
|
||||||
@@ -407,7 +419,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
|||||||
const sourcePos = {};
|
const sourcePos = {};
|
||||||
let sourceCursorY = padding;
|
let sourceCursorY = padding;
|
||||||
sourceNames.forEach((name) => {
|
sourceNames.forEach((name) => {
|
||||||
const nodeHeight = Math.max(2, sourceValueMap[name] * sourceUnitHeight);
|
const nodeHeight = sourceValueMap[name] * sourceUnitHeight;
|
||||||
sourcePos[name] = { y: sourceCursorY, h: nodeHeight };
|
sourcePos[name] = { y: sourceCursorY, h: nodeHeight };
|
||||||
sourceCursorY += nodeHeight + nodeGap;
|
sourceCursorY += nodeHeight + nodeGap;
|
||||||
});
|
});
|
||||||
@@ -436,7 +448,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
|||||||
const targetPos = {};
|
const targetPos = {};
|
||||||
let targetCursorY = targetStartY;
|
let targetCursorY = targetStartY;
|
||||||
targetNames.forEach((name) => {
|
targetNames.forEach((name) => {
|
||||||
const nodeHeight = Math.max(2, targetValueMap[name] * targetUnitHeight);
|
const nodeHeight = targetValueMap[name] * targetUnitHeight;
|
||||||
targetPos[name] = { y: targetCursorY, h: nodeHeight };
|
targetPos[name] = { y: targetCursorY, h: nodeHeight };
|
||||||
targetCursorY += nodeHeight + targetGap;
|
targetCursorY += nodeHeight + targetGap;
|
||||||
});
|
});
|
||||||
@@ -458,7 +470,10 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
|||||||
return;
|
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 sy = sourceNode.y + sourceOffset[link.source] + linkHeight / 2;
|
||||||
const ty = targetNode.y + targetOffset[link.target] + linkHeight / 2;
|
const ty = targetNode.y + targetOffset[link.target] + linkHeight / 2;
|
||||||
sourceOffset[link.source] += linkHeight;
|
sourceOffset[link.source] += linkHeight;
|
||||||
@@ -539,7 +554,12 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
|||||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||||
? layout.nodeColorIndexMap[name]
|
? layout.nodeColorIndexMap[name]
|
||||||
: index;
|
: 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);
|
const textPlacement = getSvgLabelPlacement(layout, true, labelPositionMode);
|
||||||
segments.push(
|
segments.push(
|
||||||
`<rect x="${formatSvgNumber(layout.leftX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
`<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])
|
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||||
? layout.nodeColorIndexMap[name]
|
? layout.nodeColorIndexMap[name]
|
||||||
: index;
|
: 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);
|
const textPlacement = getSvgLabelPlacement(layout, false, labelPositionMode);
|
||||||
segments.push(
|
segments.push(
|
||||||
`<rect x="${formatSvgNumber(layout.rightX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
`<rect x="${formatSvgNumber(layout.rightX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
||||||
@@ -1261,13 +1286,14 @@ Page({
|
|||||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||||
? layout.nodeColorIndexMap[name]
|
? layout.nodeColorIndexMap[name]
|
||||||
: index;
|
: index;
|
||||||
|
const textY = getClampedLabelY(node.y, node.h, height, this.data.chartPadding);
|
||||||
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
||||||
ctx.fillRect(layout.leftX, node.y, layout.nodeWidth, node.h);
|
ctx.fillRect(layout.leftX, 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, node.y + getNodeLabelCenterY(node.h));
|
ctx.fillText(name, labelPlacement.x, textY);
|
||||||
});
|
});
|
||||||
|
|
||||||
layout.targetNames.forEach((name, index) => {
|
layout.targetNames.forEach((name, index) => {
|
||||||
@@ -1276,13 +1302,14 @@ Page({
|
|||||||
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
|
||||||
? layout.nodeColorIndexMap[name]
|
? layout.nodeColorIndexMap[name]
|
||||||
: index;
|
: index;
|
||||||
|
const textY = getClampedLabelY(node.y, node.h, height, this.data.chartPadding);
|
||||||
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
|
||||||
ctx.fillRect(layout.rightX, node.y, layout.nodeWidth, node.h);
|
ctx.fillRect(layout.rightX, 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, node.y + getNodeLabelCenterY(node.h));
|
ctx.fillText(name, labelPlacement.x, textY);
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.draw();
|
ctx.draw();
|
||||||
|
|||||||
@@ -234,7 +234,7 @@
|
|||||||
.preview-canvas {
|
.preview-canvas {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
Reference in New Issue
Block a user