diff --git a/miniapp/pages/index/index.js b/miniapp/pages/index/index.js index 689d992..fc30e3a 100644 --- a/miniapp/pages/index/index.js +++ b/miniapp/pages/index/index.js @@ -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( `