Перейти к основному содержимому
clawam tools
Инструменты Документы Daoark EN Главная Главная / Преобразование данных › Главная manifest.json 0; }); var links = []; for (var i = 0; i < lines.length; i++) { var parts = lines[i].split(/\s*(?:→|->|→)\s*/); if (parts.length >= 3) { var v = parseFloat(parts[2]); if (!isNaN(v)) links.push({ source: parts[0].trim(), target: parts[1].trim(), value: v }); } } return links; } function escape(s) { return String(s).replace(/[&<>"']/g, function(c) { return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } function renderSankey(links, title) { if (links.length === 0) return ''; var nodeSet = {}; for (var i = 0; i < links.length; i++) { nodeSet[links[i].source] = true; nodeSet[links[i].target] = true; } var nodes = Object.keys(nodeSet); // 计算每节点总流量(作为列位置) var col = {}; for (var j = 0; j < links.length; j++) { if (col[links[j].source] === undefined) col[links[j].source] = 0; if (col[links[j].target] === undefined) col[links[j].target] = 0; } // 出度+入度 var total = {}; for (var k = 0; k < nodes.length; k++) total[nodes[k]] = 0; for (var m = 0; m < links.length; m++) { total[links[m].source] += links[m].value; total[links[m].target] += links[m].value; } // 简单布局:源(最左)= 仅出度,中间= 入+出,目标(最右)= 仅入度 var maxTotal = Math.max.apply(null, nodes.map(function(n) { return total[n]; })); var W = 800, H = Math.max(400, 30 * nodes.length); var padX = 100, padY = 30; var innerW = W - 2 * padX; var innerH = H - 2 * padY; var nodeW = 18; var scale = innerH / maxTotal; // 节点位置(按名字首字母排序,从上到下) var sorted = nodes.slice().sort(); var nodeY = {}; var yCursor = padY; for (var n = 0; n < sorted.length; n++) { var h = total[sorted[n]] * scale; nodeY[sorted[n]] = { y: yCursor, h: h, x: padX }; yCursor += h + 8; } // 流带起点终点 var flows = links.map(function(l) { return { source: l.source, target: l.target, value: l.value, sy: nodeY[l.source].y, ty: nodeY[l.target].y }; }); // 简化:所有节点在同一列(源在左,目标在右各一列) // 重新判断:源/目标分类 var sourcesOnly = [], targetsOnly = [], middles = []; for (var n2 = 0; n2 < nodes.length; n2++) { var n = nodes[n2]; var isSource = links.some(function(l) { return l.source === n; }); var isTarget = links.some(function(l) { return l.target === n; }); if (isSource && !isTarget) sourcesOnly.push(n); else if (!isSource && isTarget) targetsOnly.push(n); else middles.push(n); } var allOrdered = sourcesOnly.concat(middles).concat(targetsOnly); var maxCol = sourcesOnly.length > 0 ? (middles.length > 0 ? 2 : 1) : (targetsOnly.length > 0 ? 1 : 0); var colX = []; for (var c = 0; c <= maxCol; c++) colX.push(padX + c * (innerW / Math.max(maxCol, 1))); var nodeX = {}; var order = sourcesOnly.concat(middles, targetsOnly); for (var o = 0; o < order.length; o++) { var n = order[o]; if (sourcesOnly.indexOf(n) >= 0) nodeX[n] = colX[0]; else if (middles.indexOf(n) >= 0) nodeX[n] = colX[1] || colX[0]; else if (targetsOnly.indexOf(n) >= 0) nodeX[n] = colX[maxCol] || colX[0]; } // 按 y 排序并分配 var yOffset = {}; for (var kk = 0; kk < allOrdered.length; kk++) yOffset[allOrdered[kk]] = 0; // 简化:同一组(源/中/目标)内的节点 y 位置 function assignY(group) { var totalH = group.reduce(function(s, n) { return s + total[n] * scale; }, 0); var gap = (innerH - totalH) / (group.length + 1); var y = padY + gap; for (var i = 0; i < group.length; i++) { nodeY[group[i]] = { y: y, h: total[group[i]] * scale, x: nodeX[group[i]] }; y += total[group[i]] * scale + gap; } } assignY(sourcesOnly); assignY(middles); assignY(targetsOnly); var colors = ['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899','#06b6d4','#84cc16','#f97316','#6366f1']; var nodeColor = {}; for (var i = 0; i < nodes.length; i++) nodeColor[nodes[i]] = colors[i % colors.length]; var svg = ''; if (title) svg += '' + escape(title) + ''; // 流带 var srcCursor = {}, tgtCursor = {}; for (var i = 0; i < links.length; i++) { var l = links[i]; var sh = l.value * scale; var sy = (srcCursor[l.source] !== undefined ? srcCursor[l.source] : nodeY[l.source].y); var ty = (tgtCursor[l.target] !== undefined ? tgtCursor[l.target] : nodeY[l.target].y); var x1 = nodeY[l.source].x + nodeW; var x2 = nodeY[l.target].x; var midX = (x1 + x2) / 2; var color = nodeColor[l.source]; var path = 'M ' + x1 + ' ' + sy + ' C ' + midX + ' ' + sy + ' ' + midX + ' ' + ty + ' ' + x2 + ' ' + ty + ' L ' + x2 + ' ' + (ty + sh) + ' C ' + midX + ' ' + (ty + sh) + ' ' + midX + ' ' + (sy + sh) + ' ' + x1 + ' ' + (sy + sh) + ' Z'; svg += ''; srcCursor[l.source] = sy + sh; tgtCursor[l.target] = ty + sh; } // 节点矩形 for (var i = 0; i < nodes.length; i++) { var n = nodes[i]; var ny = nodeY[n]; svg += ''; svg += '' + escape(n) + ' (' + total[n].toFixed(0) + ')'; } svg += ''; return svg; } var currentSVG = ''; function doProcess() { try { var textA = getVal('text-a'); if (!textA) { showError('请输入'); return; } var title = getVal('title'); var links = parseLinks(textA); if (links.length === 0) { showError('无有效数据,需 源 → 目标 数值 格式'); return; } var svg = renderSankey(links, title); currentSVG = svg; showResult(svg, '流带数: ' + links.length + ', 节点数: ' + new Set(links.map(function(l){return l.source;}).concat(links.map(function(l){return l.target;}))).size); } catch (e) { showError('处理失败: ' + (e.message || e)); } } $('btn-main').addEventListener('click', function(e) { e.preventDefault(); doProcess(); }); $('btn-clear').addEventListener('click', function() { var inputs = document.querySelectorAll('input,textarea,select'); for (var i = 0; i < inputs.length; i++) { if (inputs[i].tagName === 'SELECT') inputs[i].selectedIndex = 0; else inputs[i].value = inputs[i].defaultValue; } var ra = $('result-area'); ra.hidden = true; ra.style.display = 'none'; var em = $('error-msg'); em.hidden = true; em.style.display = 'none'; $('btn-download').style.display = 'none'; }); $('btn-download').addEventListener('click', function() { if (!currentSVG) return; var blob = new Blob([currentSVG], { type: 'image/svg+xml' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'sankey.svg'; a.click(); URL.revokeObjectURL(url); }); })();