update at 2026-02-13 22:55:01
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const { parseTableByFileName, buildSankeyData } = require('../../utils/sankey');
|
||||
const defaultSankeyTable = require('../../data/sankey.default.js');
|
||||
const themePresets = require('../../data/theme-presets.js');
|
||||
|
||||
/**
|
||||
* 将表头标准化,便于做中英文别名匹配。
|
||||
@@ -65,11 +66,39 @@ function getFileExtension(fileName) {
|
||||
return lowerName.slice(lastDotIndex + 1);
|
||||
}
|
||||
|
||||
const FALLBACK_THEME_COLORS = ['#9b6bc2', '#7e95f7', '#4cc9f0', '#f4a261'];
|
||||
|
||||
/**
|
||||
* 节点配色:与当前画布渲染保持一致,双色交替。
|
||||
* 节点配色:使用当前主题色带循环取色。
|
||||
*/
|
||||
function getNodeColor(index) {
|
||||
return index % 2 === 0 ? '#9b6bc2' : '#7e95f7';
|
||||
function getNodeColor(index, palette) {
|
||||
const colors = Array.isArray(palette) && palette.length > 0 ? palette : FALLBACK_THEME_COLORS;
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 16 进制颜色转换为 rgba,便于控制连线透明度。
|
||||
*/
|
||||
function toRgbaColor(color, alpha) {
|
||||
const text = String(color || '').trim();
|
||||
const safeAlpha = Number.isFinite(Number(alpha)) ? Math.max(0, Math.min(1, Number(alpha))) : 1;
|
||||
const shortHex = /^#([0-9a-fA-F]{3})$/;
|
||||
const longHex = /^#([0-9a-fA-F]{6})$/;
|
||||
|
||||
let hex = '';
|
||||
if (shortHex.test(text)) {
|
||||
const raw = text.slice(1);
|
||||
hex = `${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}`;
|
||||
} else if (longHex.test(text)) {
|
||||
hex = text.slice(1);
|
||||
} else {
|
||||
return text || `rgba(155,107,194,${safeAlpha})`;
|
||||
}
|
||||
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
return `rgba(${r},${g},${b},${safeAlpha})`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,6 +166,14 @@ const DEFAULT_SANKEY_FILE_PATHS = [
|
||||
'data/sankey.xlsx',
|
||||
'miniapp/data/sankey.xlsx'
|
||||
];
|
||||
const DEFAULT_THEME_ID = 'figma-violet';
|
||||
const DEFAULT_THEME_INDEX = (() => {
|
||||
if (!Array.isArray(themePresets) || themePresets.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const index = themePresets.findIndex((item) => item && item.id === DEFAULT_THEME_ID);
|
||||
return index >= 0 ? index : 0;
|
||||
})();
|
||||
|
||||
/**
|
||||
* 数值限制,避免 UI 参数导致布局异常。
|
||||
@@ -256,6 +293,10 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
||||
|
||||
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;
|
||||
@@ -329,7 +370,8 @@ function buildSankeyLayout(links, width, height, renderOptions) {
|
||||
controlX,
|
||||
sy,
|
||||
ty,
|
||||
linkHeight
|
||||
linkHeight,
|
||||
sourceIndex: Number.isFinite(sourceIndexMap[link.source]) ? sourceIndexMap[link.source] : 0
|
||||
});
|
||||
});
|
||||
|
||||
@@ -354,6 +396,12 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
return '';
|
||||
}
|
||||
const labelPositionMode = (renderOptions && renderOptions.labelPositionMode) || 'inner';
|
||||
const themeColors =
|
||||
renderOptions &&
|
||||
Array.isArray(renderOptions.themeColors) &&
|
||||
renderOptions.themeColors.length > 0
|
||||
? renderOptions.themeColors
|
||||
: FALLBACK_THEME_COLORS;
|
||||
|
||||
const segments = [];
|
||||
segments.push(
|
||||
@@ -372,7 +420,10 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
segment.endX
|
||||
)} ${formatSvgNumber(segment.ty)}`;
|
||||
segments.push(
|
||||
`<path d="${pathData}" fill="none" stroke="#9b6bc2" stroke-opacity="0.35" stroke-width="${formatSvgNumber(
|
||||
`<path d="${pathData}" fill="none" stroke="${toRgbaColor(
|
||||
getNodeColor(segment.sourceIndex, themeColors),
|
||||
0.35
|
||||
)}" stroke-width="${formatSvgNumber(
|
||||
segment.linkHeight
|
||||
)}" stroke-linecap="round" />`
|
||||
);
|
||||
@@ -385,7 +436,7 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
segments.push(
|
||||
`<rect x="${formatSvgNumber(layout.leftX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
||||
layout.nodeWidth
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index)}" />`
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index, themeColors)}" />`
|
||||
);
|
||||
segments.push(
|
||||
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
|
||||
@@ -401,7 +452,7 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
segments.push(
|
||||
`<rect x="${formatSvgNumber(layout.rightX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
|
||||
layout.nodeWidth
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index)}" />`
|
||||
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index, themeColors)}" />`
|
||||
);
|
||||
segments.push(
|
||||
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
|
||||
@@ -416,7 +467,8 @@ function buildSankeySvgText(links, width, height, renderOptions) {
|
||||
|
||||
Page({
|
||||
data: {
|
||||
selectedThemeIndex: 1,
|
||||
selectedThemeIndex: DEFAULT_THEME_INDEX,
|
||||
themes: Array.isArray(themePresets) ? themePresets : [],
|
||||
showThemeSheet: false,
|
||||
uploadMessage: '默认加载 data/sankey.xlsx 中...',
|
||||
parseError: '',
|
||||
@@ -525,6 +577,37 @@ Page({
|
||||
this.setData({ showThemeSheet: false });
|
||||
},
|
||||
|
||||
/**
|
||||
* 返回当前选中主题色带。
|
||||
*/
|
||||
getCurrentThemeColors() {
|
||||
const themes = this.data.themes || [];
|
||||
const selectedTheme = themes[this.data.selectedThemeIndex] || themes[0];
|
||||
if (!selectedTheme || !Array.isArray(selectedTheme.colors) || selectedTheme.colors.length === 0) {
|
||||
return FALLBACK_THEME_COLORS;
|
||||
}
|
||||
return selectedTheme.colors;
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择主题后立即生效,并关闭底部主题弹层。
|
||||
*/
|
||||
onSelectTheme(e) {
|
||||
const index = Number(e.currentTarget.dataset.index);
|
||||
if (Number.isNaN(index)) {
|
||||
return;
|
||||
}
|
||||
this.setData(
|
||||
{
|
||||
selectedThemeIndex: index,
|
||||
showThemeSheet: false
|
||||
},
|
||||
() => {
|
||||
this.drawSankey();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新“节点间距”下拉值并立即重绘。
|
||||
*/
|
||||
@@ -931,6 +1014,7 @@ Page({
|
||||
drawSankey() {
|
||||
const rawLinks = this.data.sankeyLinks || [];
|
||||
const links = applyDirection(rawLinks, this.data.direction);
|
||||
const themeColors = this.getCurrentThemeColors();
|
||||
const query = wx.createSelectorQuery().in(this);
|
||||
query.select('#sankeyCanvas').boundingClientRect();
|
||||
query.exec((res) => {
|
||||
@@ -960,7 +1044,7 @@ Page({
|
||||
}
|
||||
|
||||
layout.linkSegments.forEach((segment) => {
|
||||
ctx.setStrokeStyle('rgba(155,107,194,0.35)');
|
||||
ctx.setStrokeStyle(toRgbaColor(getNodeColor(segment.sourceIndex, themeColors), 0.35));
|
||||
ctx.setLineWidth(segment.linkHeight);
|
||||
ctx.setLineCap('round');
|
||||
ctx.beginPath();
|
||||
@@ -979,7 +1063,7 @@ Page({
|
||||
layout.sourceNames.forEach((name, index) => {
|
||||
const node = layout.sourcePos[name];
|
||||
const labelPlacement = getCanvasLabelPlacement(layout, true, this.data.labelPositionMode);
|
||||
ctx.setFillStyle(getNodeColor(index));
|
||||
ctx.setFillStyle(getNodeColor(index, themeColors));
|
||||
ctx.fillRect(layout.leftX, node.y, layout.nodeWidth, node.h);
|
||||
ctx.setFillStyle('#4e5969');
|
||||
ctx.setFontSize(10);
|
||||
@@ -991,7 +1075,7 @@ Page({
|
||||
layout.targetNames.forEach((name, index) => {
|
||||
const node = layout.targetPos[name];
|
||||
const labelPlacement = getCanvasLabelPlacement(layout, false, this.data.labelPositionMode);
|
||||
ctx.setFillStyle(getNodeColor(index));
|
||||
ctx.setFillStyle(getNodeColor(index, themeColors));
|
||||
ctx.fillRect(layout.rightX, node.y, layout.nodeWidth, node.h);
|
||||
ctx.setFillStyle('#4e5969');
|
||||
ctx.setFontSize(10);
|
||||
@@ -1049,6 +1133,7 @@ Page({
|
||||
onExportSvg() {
|
||||
const rawLinks = this.data.sankeyLinks || [];
|
||||
const links = applyDirection(rawLinks, this.data.direction);
|
||||
const themeColors = this.getCurrentThemeColors();
|
||||
if (!Array.isArray(links) || links.length === 0) {
|
||||
wx.showToast({
|
||||
title: '暂无可导出的数据',
|
||||
@@ -1073,7 +1158,8 @@ Page({
|
||||
nodeGap: this.data.nodeGap,
|
||||
chartPadding: this.data.chartPadding,
|
||||
labelPositionMode: this.data.labelPositionMode,
|
||||
targetAlignMode: this.data.targetAlignMode
|
||||
targetAlignMode: this.data.targetAlignMode,
|
||||
themeColors
|
||||
});
|
||||
if (!svgText) {
|
||||
wx.showToast({
|
||||
|
||||
Reference in New Issue
Block a user