update at 2026-02-13 23:00:20

This commit is contained in:
douboer@gmail.com
2026-02-13 23:00:20 +08:00
parent 1ef046719c
commit f5f6a42bfa
3 changed files with 188 additions and 40 deletions

View File

@@ -174,6 +174,10 @@ const DEFAULT_THEME_INDEX = (() => {
const index = themePresets.findIndex((item) => item && item.id === DEFAULT_THEME_ID);
return index >= 0 ? index : 0;
})();
const THEME_ROW_HEIGHT_PX = 36;
const THEME_VISIBLE_ROW_COUNT = 6;
const THEME_LIST_HEIGHT_PX = THEME_ROW_HEIGHT_PX * THEME_VISIBLE_ROW_COUNT;
const THEME_LIST_EDGE_SPACER_PX = (THEME_LIST_HEIGHT_PX - THEME_ROW_HEIGHT_PX) / 2;
/**
* 数值限制,避免 UI 参数导致布局异常。
@@ -186,6 +190,19 @@ function clampNumber(value, min, max, fallback) {
return Math.min(max, Math.max(min, normalized));
}
/**
* 主题弹层打开时的滚动定位:
* 通过上下占位留白,让任意索引都可滚动到视图中心。
*/
function getThemeListScrollTopByIndex(index, totalCount) {
const total = Number(totalCount);
if (!Number.isFinite(total) || total <= 0) {
return 0;
}
const safeIndex = clampNumber(index, 0, total - 1, 0);
return safeIndex * THEME_ROW_HEIGHT_PX;
}
/**
* 统一错误文案:
* - xlsx 解析能力缺失时,固定提示用户去“构建 npm”
@@ -286,17 +303,23 @@ function buildSankeyLayout(links, width, height, renderOptions) {
const sourceValueMap = {};
const targetValueMap = {};
const nodeColorIndexMap = {};
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);
if (!Object.prototype.hasOwnProperty.call(nodeColorIndexMap, link.source)) {
nodeColorIndexMap[link.source] = nodeColorIndexCursor;
nodeColorIndexCursor += 1;
}
if (!Object.prototype.hasOwnProperty.call(nodeColorIndexMap, link.target)) {
nodeColorIndexMap[link.target] = nodeColorIndexCursor;
nodeColorIndexCursor += 1;
}
});
const sourceNames = Object.keys(sourceValueMap);
const targetNames = Object.keys(targetValueMap);
const sourceIndexMap = {};
sourceNames.forEach((name, index) => {
sourceIndexMap[name] = index;
});
const totalValue = sourceNames.reduce((sum, name) => sum + sourceValueMap[name], 0);
if (totalValue <= 0) {
return null;
@@ -371,7 +394,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
sy,
ty,
linkHeight,
sourceIndex: Number.isFinite(sourceIndexMap[link.source]) ? sourceIndexMap[link.source] : 0
sourceIndex: Number.isFinite(nodeColorIndexMap[link.source]) ? nodeColorIndexMap[link.source] : 0
});
});
@@ -383,6 +406,7 @@ function buildSankeyLayout(links, width, height, renderOptions) {
targetNames,
sourcePos,
targetPos,
nodeColorIndexMap,
linkSegments
};
}
@@ -431,12 +455,15 @@ 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;
const textY = node.y + getNodeLabelCenterY(node.h);
const textPlacement = getSvgLabelPlacement(layout, true, labelPositionMode);
segments.push(
`<rect x="${formatSvgNumber(layout.leftX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
layout.nodeWidth
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index, themeColors)}" />`
)}" 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="${
@@ -447,12 +474,15 @@ function buildSankeySvgText(links, width, height, renderOptions) {
layout.targetNames.forEach((name, index) => {
const node = layout.targetPos[name];
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
? layout.nodeColorIndexMap[name]
: index;
const textY = node.y + getNodeLabelCenterY(node.h);
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(index, themeColors)}" />`
)}" 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="${
@@ -470,6 +500,10 @@ Page({
selectedThemeIndex: DEFAULT_THEME_INDEX,
themes: Array.isArray(themePresets) ? themePresets : [],
showThemeSheet: false,
themeListScrollTop: getThemeListScrollTopByIndex(
DEFAULT_THEME_INDEX,
Array.isArray(themePresets) ? themePresets.length : 0
),
uploadMessage: '默认加载 data/sankey.xlsx 中...',
parseError: '',
buildError: '',
@@ -478,6 +512,9 @@ Page({
sourceDataColumn: null,
sourceDescriptionColumns: [],
targetDescriptionColumns: [],
sectionVisibleSourceData: true,
sectionVisibleSourceDesc: true,
sectionVisibleTargetDesc: true,
nodesCount: 0,
linksCount: 0,
droppedRows: 0,
@@ -567,7 +604,32 @@ Page({
* 主题选择按钮点击后,切换底部选择器。
*/
onToggleThemeSheet() {
this.setData({ showThemeSheet: !this.data.showThemeSheet });
const nextVisible = !this.data.showThemeSheet;
if (!nextVisible) {
this.setData({ showThemeSheet: false });
return;
}
const targetScrollTop = getThemeListScrollTopByIndex(
this.data.selectedThemeIndex,
(this.data.themes || []).length
);
this.setData(
{
showThemeSheet: true,
themeListScrollTop: 0
},
() => {
const applyScrollTop = () => {
this.setData({ themeListScrollTop: targetScrollTop });
};
if (typeof wx.nextTick === 'function') {
wx.nextTick(applyScrollTop);
return;
}
setTimeout(applyScrollTop, 0);
}
);
},
/**
@@ -577,6 +639,24 @@ Page({
this.setData({ showThemeSheet: false });
},
/**
* 切换数据映射区块展开/收起状态,对齐 Web 的 expand/zhedie 行为。
*/
onToggleSection(e) {
const section = String((e.currentTarget.dataset && e.currentTarget.dataset.section) || '');
if (section === 'sourceData') {
this.setData({ sectionVisibleSourceData: !this.data.sectionVisibleSourceData });
return;
}
if (section === 'sourceDesc') {
this.setData({ sectionVisibleSourceDesc: !this.data.sectionVisibleSourceDesc });
return;
}
if (section === 'targetDesc') {
this.setData({ sectionVisibleTargetDesc: !this.data.sectionVisibleTargetDesc });
}
},
/**
* 返回当前选中主题色带。
*/
@@ -1063,7 +1143,10 @@ Page({
layout.sourceNames.forEach((name, index) => {
const node = layout.sourcePos[name];
const labelPlacement = getCanvasLabelPlacement(layout, true, this.data.labelPositionMode);
ctx.setFillStyle(getNodeColor(index, themeColors));
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
? layout.nodeColorIndexMap[name]
: index;
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
ctx.fillRect(layout.leftX, node.y, layout.nodeWidth, node.h);
ctx.setFillStyle('#4e5969');
ctx.setFontSize(10);
@@ -1075,7 +1158,10 @@ Page({
layout.targetNames.forEach((name, index) => {
const node = layout.targetPos[name];
const labelPlacement = getCanvasLabelPlacement(layout, false, this.data.labelPositionMode);
ctx.setFillStyle(getNodeColor(index, themeColors));
const colorIndex = Number.isFinite(layout.nodeColorIndexMap[name])
? layout.nodeColorIndexMap[name]
: index;
ctx.setFillStyle(getNodeColor(colorIndex, themeColors));
ctx.fillRect(layout.rightX, node.y, layout.nodeWidth, node.h);
ctx.setFillStyle('#4e5969');
ctx.setFontSize(10);