Spaces:
Running
You already have a functional Sankey diagram webapp built with HTML + JavaScript + D3.js.
Browse filesNow, I want you to update and improve the existing app, not recreate it from zero.
Please apply the following changes and corrections to the current implementation:
⚙️ CORE FIXES
Make node dragging fully interactive
When a node is dragged, all connected links must move dynamically in real-time, just like in Plotly Sankey charts.
Use d3.sankeyLinkHorizontal() to recompute the link paths whenever a node moves.
Update each node’s internal coordinates (d.x0, d.x1, d.y0, d.y1) during drag events.
Example snippet for the drag behavior:
.on("drag", function(event, d) {
d.x0 = event.x;
d.x1 = event.x + (d.x1 - d.x0);
d.y0 = event.y;
d.y1 = event.y + (d.y1 - d.y0);
nodePositions[d.index] = { x: d.x0, y: d.y0 };
d3.select(this).attr("transform", `translate(${d.x0},${d.y0})`);
svg.selectAll("path").attr("d", d3.sankeyLinkHorizontal());
});
Fix the “Export to CSV” button (currently broken)
Replace the existing export logic with a stable version that works across all browsers:
function exportToCSV() {
if (!sankeyData.nodes.length || !sankeyData.links.length) {
alert("No data to export.");
return;
}
let csvContent = "Source,Target,Value\r\n";
sankeyData.links.forEach(link => {
const src = sankeyData.nodes[link.source]?.name || "";
const tgt = sankeyData.nodes[link.target]?.name || "";
csvContent += `"${src}","${tgt}",${link.value}\r\n`;
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = "sankey_data.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
🧩 ENHANCEMENTS TO THE EXISTING APP
Allow individual node colors
In the node list UI, add a color picker (<input type="color">) for each node.
When a color is changed, update that node’s color property and re-render the diagram.
Each link should automatically inherit the color of its source node (with transparency).
Example:
.attr("stroke", d => {
const srcColor = sankeyData.nodes[d.source]?.color || defaultLinkColor;
return `rgba(${hexToRgb(srcColor)},${linkAlpha})`;
});
Add alpha (opacity) control for links
Create a global slider input (0.1 → 1.0) to control the transparency of all links.
The alpha value should apply to link colors dynamically.
Enable quick renaming by double-click
When double-clicking a node label, open a prompt() to change its name.
Update sankeyData.nodes[d.index].name and re-render.
LocalStorage autosave
Save the current sankeyData JSON automatically on every change (add/remove/move/edit).
On page load, restore the saved diagram if available.
Use:
localStorage.setItem('sankeyData', JSON.stringify(sankeyData));
and
const saved = localStorage.getItem('sankeyData');
if (saved) sankeyData = JSON.parse(saved);
Import from CSV
Add a button to upload and import a .csv file with columns Source,Target,Value.
Use FileReader to parse it and rebuild the sankeyData structure.
Automatically create missing nodes.
Smooth animations
When updating or moving nodes, use a short D3 transition (e.g., duration(500)) to make movement smoother.
🎨 VISUAL / UI IMPROVEMENTS
Better UI using TailwindCSS or DaisyUI
Keep the current structure but make it look cleaner:
Sidebar with collapsible panels for:
Appearance (colors, alpha, node width, height, text position)
Data (nodes & links lists)
Import/Export (buttons)
Main area for the diagram (#diagram-container with #sankey-diagram inside).
Light gray background for the diagram, rounded corners, subtle box shadow.
Optional small extras
Tooltip showing link value when hovering over a link.
“Center Diagram” button to reset all node positions to the auto layout.
Keep all features in one single HTML file (no external JS files).
📦 TECHNICAL REQUIREMENTS
Keep using D3.js (version 7+).
No need for external frameworks beyond Tailwind or DaisyUI (optional for styling).
Maintain compatibility with modern browsers (Chrome, Edge, Firefox).
The code must remain standalone and work by simply opening the HTML file locally.
Preserve all existing working features (adding/deleting nodes, lists, controls, etc.).
✅ SUMMARY OF GOALS
Upgrade the existing D3 Sankey editor to:
Have truly interactive dragging (links follow nodes).
Fix CSV export.
Support color per node + link alpha.
Enable local save/restore.
Add import from CSV.
Keep a clean modern UI.
Do not rebuild from scratch — only improve and extend the current implementation.
That’s it — please update the existing code accordingly.
- index.html +67 -95
- script.js +254 -93
- style.css +4 -3
|
@@ -11,73 +11,40 @@
|
|
| 11 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 13 |
</head>
|
| 14 |
-
<body class="bg-gray-
|
| 15 |
-
<
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
<!-- Diagram Preview Section -->
|
| 21 |
-
<section class="mb-8 bg-white rounded-xl shadow-md overflow-hidden">
|
| 22 |
-
<div class="p-4 border-b border-gray-200">
|
| 23 |
-
<h2 class="text-xl font-semibold text-gray-700">Live Preview</h2>
|
| 24 |
-
</div>
|
| 25 |
-
<div id="diagram-container" class="p-4 h-96 w-full relative">
|
| 26 |
-
<svg id="sankey-diagram" class="w-full h-full"></svg>
|
| 27 |
-
<div class="absolute top-4 right-4 flex space-x-2">
|
| 28 |
-
<button id="update-diagram" class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded-md text-sm shadow-md transition">
|
| 29 |
-
<i data-feather="refresh-cw" class="mr-1"></i> Update
|
| 30 |
-
</button>
|
| 31 |
-
<button id="export-csv" class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-md text-sm shadow-md transition">
|
| 32 |
-
<i data-feather="download" class="mr-1"></i> Export CSV
|
| 33 |
-
</button>
|
| 34 |
-
</div>
|
| 35 |
-
</div>
|
| 36 |
-
</section>
|
| 37 |
-
<!-- Control Panel Section -->
|
| 38 |
-
<section class="bg-white rounded-xl shadow-md overflow-hidden">
|
| 39 |
-
<div class="p-4 border-b border-gray-200">
|
| 40 |
-
<h2 class="text-xl font-semibold text-gray-700">Control Panel</h2>
|
| 41 |
</div>
|
| 42 |
|
| 43 |
-
<div class="
|
| 44 |
-
<!--
|
| 45 |
<div class="bg-gray-50 p-4 rounded-lg">
|
| 46 |
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 47 |
-
<i data-feather="
|
| 48 |
</h3>
|
| 49 |
-
<div class="
|
| 50 |
-
<
|
| 51 |
-
<
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
</div>
|
|
|
|
| 56 |
</div>
|
| 57 |
|
| 58 |
-
<!--
|
| 59 |
-
<div class="bg-gray-50 p-4 rounded-lg">
|
| 60 |
-
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 61 |
-
<i data-feather="link" class="mr-2"></i> Links
|
| 62 |
-
</h3>
|
| 63 |
-
<div class="space-y-3">
|
| 64 |
-
<div class="grid grid-cols-3 gap-2">
|
| 65 |
-
<select id="source-node" class="col-span-1 p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
|
| 66 |
-
<select id="target-node" class="col-span-1 p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
|
| 67 |
-
<input type="number" id="link-value" placeholder="Value" min="1" value="1" class="col-span-1 p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
| 68 |
-
</div>
|
| 69 |
-
<div class="flex justify-end">
|
| 70 |
-
<button id="add-link" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition">Add Link</button>
|
| 71 |
-
</div>
|
| 72 |
-
<div id="links-list" class="max-h-40 overflow-y-auto border rounded-md p-2 space-y-2"></div>
|
| 73 |
-
</div>
|
| 74 |
-
</div>
|
| 75 |
-
<!-- Text Customization -->
|
| 76 |
<div class="bg-gray-50 p-4 rounded-lg">
|
| 77 |
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 78 |
-
<i data-feather="
|
| 79 |
</h3>
|
| 80 |
-
<div class="
|
| 81 |
<div>
|
| 82 |
<label class="block text-sm text-gray-600 mb-1">Text Color</label>
|
| 83 |
<input type="color" id="text-color" value="#ffffff" class="w-full h-10">
|
|
@@ -90,67 +57,72 @@
|
|
| 90 |
<option value="bottom">Bottom</option>
|
| 91 |
</select>
|
| 92 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
</div>
|
| 94 |
</div>
|
| 95 |
|
| 96 |
-
<!--
|
| 97 |
<div class="bg-gray-50 p-4 rounded-lg">
|
| 98 |
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 99 |
-
<i data-feather="
|
| 100 |
</h3>
|
| 101 |
-
<div class="
|
| 102 |
-
<div>
|
| 103 |
-
<
|
| 104 |
-
<
|
| 105 |
-
</div>
|
| 106 |
-
<div>
|
| 107 |
-
<label class="block text-sm text-gray-600 mb-1">Link Color</label>
|
| 108 |
-
<input type="color" id="link-color" value="#93c5fd" class="w-full h-10">
|
| 109 |
</div>
|
|
|
|
| 110 |
</div>
|
| 111 |
</div>
|
| 112 |
|
| 113 |
-
<!--
|
| 114 |
<div class="bg-gray-50 p-4 rounded-lg">
|
| 115 |
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 116 |
-
<i data-feather="
|
| 117 |
</h3>
|
| 118 |
<div class="space-y-3">
|
| 119 |
-
<div>
|
| 120 |
-
<
|
| 121 |
-
<
|
| 122 |
-
|
| 123 |
-
<div>
|
| 124 |
-
<label class="block text-sm text-gray-600 mb-1">Diagram Height</label>
|
| 125 |
-
<input type="range" id="diagram-height" min="300" max="800" value="400" class="w-full">
|
| 126 |
</div>
|
| 127 |
-
<div>
|
| 128 |
-
<
|
| 129 |
-
<label class="inline-flex items-center">
|
| 130 |
-
<input type="checkbox" id="enable-dragging" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
| 131 |
-
<span class="ml-2">Allow node dragging</span>
|
| 132 |
-
</label>
|
| 133 |
</div>
|
| 134 |
-
<
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
-
</div>
|
| 138 |
-
</
|
| 139 |
-
</main>
|
| 140 |
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
-
<!-- Component scripts -->
|
| 144 |
-
<script src="components/navbar.js"></script>
|
| 145 |
-
<script src="components/footer.js"></script>
|
| 146 |
-
|
| 147 |
-
<!-- Main app script -->
|
| 148 |
-
<script src="script.js"></script>
|
| 149 |
-
|
| 150 |
<!-- Initialize feather icons -->
|
| 151 |
<script>
|
| 152 |
feather.replace();
|
| 153 |
</script>
|
| 154 |
-
|
|
|
|
|
|
|
| 155 |
</body>
|
| 156 |
-
</html>
|
|
|
|
| 11 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 13 |
</head>
|
| 14 |
+
<body class="bg-gray-100 min-h-screen flex">
|
| 15 |
+
<div class="flex flex-1">
|
| 16 |
+
<!-- Sidebar -->
|
| 17 |
+
<div class="w-80 bg-white shadow-lg flex flex-col">
|
| 18 |
+
<div class="p-4 border-b">
|
| 19 |
+
<h1 class="text-xl font-bold text-gray-800">Sankey Flow Wizard 🧙♂️</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
</div>
|
| 21 |
|
| 22 |
+
<div class="flex-1 overflow-y-auto p-4 space-y-6">
|
| 23 |
+
<!-- Import/Export -->
|
| 24 |
<div class="bg-gray-50 p-4 rounded-lg">
|
| 25 |
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 26 |
+
<i data-feather="upload" class="mr-2"></i> Import/Export
|
| 27 |
</h3>
|
| 28 |
+
<div class="grid grid-cols-2 gap-2">
|
| 29 |
+
<button id="import-csv" class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-2 rounded-md text-sm transition">
|
| 30 |
+
<i data-feather="upload" class="mr-1"></i> Import CSV
|
| 31 |
+
</button>
|
| 32 |
+
<button id="export-csv" class="bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded-md text-sm transition">
|
| 33 |
+
<i data-feather="download" class="mr-1"></i> Export CSV
|
| 34 |
+
</button>
|
| 35 |
+
<button id="center-diagram" class="bg-purple-500 hover:bg-purple-600 text-white px-3 py-2 rounded-md text-sm transition col-span-2">
|
| 36 |
+
<i data-feather="crosshair" class="mr-1"></i> Center Diagram
|
| 37 |
+
</button>
|
| 38 |
</div>
|
| 39 |
+
<input type="file" id="csv-file" accept=".csv" class="hidden">
|
| 40 |
</div>
|
| 41 |
|
| 42 |
+
<!-- Appearance -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
<div class="bg-gray-50 p-4 rounded-lg">
|
| 44 |
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 45 |
+
<i data-feather="eye" class="mr-2"></i> Appearance
|
| 46 |
</h3>
|
| 47 |
+
<div class="space-y-4">
|
| 48 |
<div>
|
| 49 |
<label class="block text-sm text-gray-600 mb-1">Text Color</label>
|
| 50 |
<input type="color" id="text-color" value="#ffffff" class="w-full h-10">
|
|
|
|
| 57 |
<option value="bottom">Bottom</option>
|
| 58 |
</select>
|
| 59 |
</div>
|
| 60 |
+
<div>
|
| 61 |
+
<label class="block text-sm text-gray-600 mb-1">Node Width</label>
|
| 62 |
+
<input type="range" id="node-width" min="10" max="50" value="20" class="w-full">
|
| 63 |
+
</div>
|
| 64 |
+
<div>
|
| 65 |
+
<label class="block text-sm text-gray-600 mb-1">Link Alpha</label>
|
| 66 |
+
<input type="range" id="link-alpha" min="0.1" max="1" step="0.1" value="0.5" class="w-full">
|
| 67 |
+
</div>
|
| 68 |
+
<div>
|
| 69 |
+
<label class="block text-sm text-gray-600 mb-1">Diagram Height</label>
|
| 70 |
+
<input type="range" id="diagram-height" min="300" max="800" value="400" class="w-full">
|
| 71 |
+
</div>
|
| 72 |
</div>
|
| 73 |
</div>
|
| 74 |
|
| 75 |
+
<!-- Nodes Management -->
|
| 76 |
<div class="bg-gray-50 p-4 rounded-lg">
|
| 77 |
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 78 |
+
<i data-feather="circle" class="mr-2"></i> Nodes
|
| 79 |
</h3>
|
| 80 |
+
<div class="space-y-3">
|
| 81 |
+
<div class="flex space-x-2">
|
| 82 |
+
<input type="text" id="node-name" placeholder="Node name" class="flex-1 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
| 83 |
+
<button id="add-node" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition">Add</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
</div>
|
| 85 |
+
<div id="nodes-list" class="max-h-60 overflow-y-auto border rounded-md p-2 space-y-2"></div>
|
| 86 |
</div>
|
| 87 |
</div>
|
| 88 |
|
| 89 |
+
<!-- Links Management -->
|
| 90 |
<div class="bg-gray-50 p-4 rounded-lg">
|
| 91 |
<h3 class="font-medium text-gray-700 mb-3 flex items-center">
|
| 92 |
+
<i data-feather="link" class="mr-2"></i> Links
|
| 93 |
</h3>
|
| 94 |
<div class="space-y-3">
|
| 95 |
+
<div class="grid grid-cols-3 gap-2">
|
| 96 |
+
<select id="source-node" class="col-span-1 p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
|
| 97 |
+
<select id="target-node" class="col-span-1 p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
|
| 98 |
+
<input type="number" id="link-value" placeholder="Value" min="1" value="1" class="col-span-1 p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
|
|
|
|
| 99 |
</div>
|
| 100 |
+
<div class="flex justify-end">
|
| 101 |
+
<button id="add-link" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition">Add Link</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
</div>
|
| 103 |
+
<div id="links-list" class="max-h-60 overflow-y-auto border rounded-md p-2 space-y-2"></div>
|
| 104 |
</div>
|
| 105 |
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
|
|
|
| 108 |
|
| 109 |
+
<!-- Main Content -->
|
| 110 |
+
<div class="flex-1 flex flex-col">
|
| 111 |
+
<!-- Diagram Area -->
|
| 112 |
+
<div class="flex-1 p-6">
|
| 113 |
+
<div id="diagram-container" class="bg-gray-200 rounded-xl shadow-md h-full relative">
|
| 114 |
+
<svg id="sankey-diagram" class="w-full h-full"></svg>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
<!-- Initialize feather icons -->
|
| 121 |
<script>
|
| 122 |
feather.replace();
|
| 123 |
</script>
|
| 124 |
+
|
| 125 |
+
<!-- Main app script -->
|
| 126 |
+
<script src="script.js"></script>
|
| 127 |
</body>
|
| 128 |
+
</html>
|
|
@@ -2,10 +2,10 @@
|
|
| 2 |
// Sankey diagram data structure with example data
|
| 3 |
let sankeyData = {
|
| 4 |
nodes: [
|
| 5 |
-
{ name: "Source" },
|
| 6 |
-
{ name: "Process" },
|
| 7 |
-
{ name: "Output A" },
|
| 8 |
-
{ name: "Output B" }
|
| 9 |
],
|
| 10 |
links: [
|
| 11 |
{ source: 0, target: 1, value: 100 },
|
|
@@ -14,23 +14,48 @@ let sankeyData = {
|
|
| 14 |
]
|
| 15 |
};
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
// Store node positions for dragging
|
| 18 |
let nodePositions = {};
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
// Initialize the diagram
|
| 21 |
function initDiagram() {
|
| 22 |
const svg = d3.select("#sankey-diagram");
|
| 23 |
svg.selectAll("*").remove();
|
|
|
|
| 24 |
// Early return if SVG container doesn't exist
|
| 25 |
if (!svg.node()) return;
|
| 26 |
-
|
|
|
|
| 27 |
const width = document.getElementById('diagram-container').clientWidth;
|
| 28 |
const height = parseInt(document.getElementById('diagram-height').value);
|
|
|
|
| 29 |
|
| 30 |
const sankey = d3.sankey()
|
| 31 |
.nodeWidth(parseInt(document.getElementById('node-width').value))
|
| 32 |
.nodePadding(20)
|
| 33 |
.extent([[1, 1], [width - 1, height - 6]]);
|
|
|
|
| 34 |
// Generate the sankey diagram
|
| 35 |
const graph = sankey({
|
| 36 |
nodes: sankeyData.nodes.map(d => Object.assign({}, d)),
|
|
@@ -39,23 +64,34 @@ function initDiagram() {
|
|
| 39 |
|
| 40 |
const nodes = graph.nodes;
|
| 41 |
const links = graph.links;
|
| 42 |
-
|
|
|
|
| 43 |
svg.attr("width", width)
|
| 44 |
.attr("height", height)
|
| 45 |
.attr("viewBox", [0, 0, width, height])
|
| 46 |
.attr("style", "max-width: 100%; height: auto;");
|
| 47 |
|
| 48 |
// Add links
|
| 49 |
-
svg.append("g")
|
| 50 |
.attr("fill", "none")
|
| 51 |
-
.attr("stroke-opacity", 0.5)
|
| 52 |
.selectAll("path")
|
| 53 |
.data(links)
|
| 54 |
.join("path")
|
| 55 |
.attr("d", d3.sankeyLinkHorizontal())
|
| 56 |
-
.attr("stroke",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
.attr("stroke-width", d => Math.max(1, d.width))
|
| 58 |
-
.style("mix-blend-mode", "multiply")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
// Add nodes with better styling
|
| 60 |
const node = svg.append("g")
|
| 61 |
.selectAll("g")
|
|
@@ -72,26 +108,32 @@ function initDiagram() {
|
|
| 72 |
node.append("rect")
|
| 73 |
.attr("height", d => d.y1 - d.y0)
|
| 74 |
.attr("width", d => d.x1 - d.x0)
|
| 75 |
-
.attr("fill",
|
| 76 |
.attr("stroke", "#fff")
|
| 77 |
.attr("stroke-width", 1)
|
| 78 |
.attr("rx", 3) // Rounded corners
|
| 79 |
.attr("ry", 3);
|
| 80 |
|
| 81 |
-
// Add drag behavior
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
.
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
// Add text with customizable position
|
| 97 |
const textPosition = document.getElementById('text-position').value;
|
|
@@ -119,7 +161,18 @@ function initDiagram() {
|
|
| 119 |
.text(d => d.name)
|
| 120 |
.attr("fill", document.getElementById('text-color').value)
|
| 121 |
.attr("font-size", "12px")
|
| 122 |
-
.attr("font-weight", "bold")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
// Update UI lists
|
|
@@ -133,9 +186,12 @@ function updateLists() {
|
|
| 133 |
nodeElement.className = 'flex items-center justify-between bg-white p-2 rounded border border-gray-200';
|
| 134 |
nodeElement.innerHTML = `
|
| 135 |
<span>${node.name}</span>
|
| 136 |
-
<
|
| 137 |
-
<
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
`;
|
| 140 |
nodesList.appendChild(nodeElement);
|
| 141 |
});
|
|
@@ -152,7 +208,7 @@ function updateLists() {
|
|
| 152 |
linkElement.className = 'flex items-center justify-between bg-white p-2 rounded border border-gray-200';
|
| 153 |
linkElement.innerHTML = `
|
| 154 |
<span>${sourceNode} → ${targetNode} (${link.value})</span>
|
| 155 |
-
<button class="text-red-500 hover:text-red-700" data-index="${index}">
|
| 156 |
<i data-feather="trash-2" width="16"></i>
|
| 157 |
</button>
|
| 158 |
`;
|
|
@@ -185,23 +241,101 @@ function updateNodeDropdowns() {
|
|
| 185 |
|
| 186 |
// Export to CSV function
|
| 187 |
function exportToCSV() {
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
sankeyData.links.forEach(link => {
|
| 191 |
-
const
|
| 192 |
-
const
|
| 193 |
-
csvContent += `"${
|
| 194 |
});
|
| 195 |
-
|
| 196 |
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
| 197 |
const url = URL.createObjectURL(blob);
|
| 198 |
-
const
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
}
|
| 206 |
|
| 207 |
// Event Listeners
|
|
@@ -210,12 +344,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 210 |
document.getElementById('add-node').addEventListener('click', () => {
|
| 211 |
const nodeName = document.getElementById('node-name').value.trim();
|
| 212 |
if (nodeName) {
|
| 213 |
-
sankeyData.nodes.push({ name: nodeName });
|
| 214 |
document.getElementById('node-name').value = '';
|
| 215 |
updateLists();
|
| 216 |
initDiagram();
|
|
|
|
| 217 |
}
|
| 218 |
});
|
|
|
|
| 219 |
// Add link
|
| 220 |
document.getElementById('add-link').addEventListener('click', () => {
|
| 221 |
const sourceIndex = parseInt(document.getElementById('source-node').value);
|
|
@@ -231,75 +367,100 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 231 |
document.getElementById('link-value').value = '';
|
| 232 |
updateLists();
|
| 233 |
initDiagram();
|
|
|
|
| 234 |
}
|
| 235 |
});
|
| 236 |
|
| 237 |
-
// Manual diagram update
|
| 238 |
-
document.getElementById('update-diagram').addEventListener('click', initDiagram);
|
| 239 |
-
|
| 240 |
// Export to CSV
|
| 241 |
document.getElementById('export-csv').addEventListener('click', exportToCSV);
|
| 242 |
|
| 243 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
document.addEventListener('click', (e) => {
|
| 245 |
-
if (e.target.closest('
|
| 246 |
-
const button = e.target.closest('
|
| 247 |
const index = parseInt(button.getAttribute('data-index'));
|
| 248 |
-
const listId = button.closest('div').id;
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
updateLists();
|
| 283 |
initDiagram();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
}
|
| 285 |
});
|
| 286 |
|
| 287 |
// Color and options changes
|
| 288 |
-
document.getElementById('node-color').addEventListener('input', initDiagram);
|
| 289 |
-
document.getElementById('link-color').addEventListener('input', initDiagram);
|
| 290 |
document.getElementById('text-color').addEventListener('input', initDiagram);
|
| 291 |
document.getElementById('text-position').addEventListener('change', initDiagram);
|
| 292 |
document.getElementById('node-width').addEventListener('input', initDiagram);
|
| 293 |
document.getElementById('diagram-height').addEventListener('input', initDiagram);
|
| 294 |
-
document.getElementById('
|
| 295 |
-
|
| 296 |
-
// Reset diagram
|
| 297 |
-
document.getElementById('reset-diagram').addEventListener('click', () => {
|
| 298 |
-
sankeyData = { nodes: [], links: [] };
|
| 299 |
-
nodePositions = {};
|
| 300 |
-
updateLists();
|
| 301 |
-
initDiagram();
|
| 302 |
-
});
|
| 303 |
|
| 304 |
// Initialize dropdowns and lists
|
| 305 |
updateLists();
|
|
|
|
| 2 |
// Sankey diagram data structure with example data
|
| 3 |
let sankeyData = {
|
| 4 |
nodes: [
|
| 5 |
+
{ name: "Source", color: "#4f46e5" },
|
| 6 |
+
{ name: "Process", color: "#4f46e5" },
|
| 7 |
+
{ name: "Output A", color: "#4f46e5" },
|
| 8 |
+
{ name: "Output B", color: "#4f46e5" }
|
| 9 |
],
|
| 10 |
links: [
|
| 11 |
{ source: 0, target: 1, value: 100 },
|
|
|
|
| 14 |
]
|
| 15 |
};
|
| 16 |
|
| 17 |
+
// Load saved data from localStorage
|
| 18 |
+
const saved = localStorage.getItem('sankeyData');
|
| 19 |
+
if (saved) {
|
| 20 |
+
sankeyData = JSON.parse(saved);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
// Store node positions for dragging
|
| 24 |
let nodePositions = {};
|
| 25 |
|
| 26 |
+
// Utility function to convert hex to RGB
|
| 27 |
+
function hexToRgb(hex) {
|
| 28 |
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
| 29 |
+
return result ? {
|
| 30 |
+
r: parseInt(result[1], 16),
|
| 31 |
+
g: parseInt(result[2], 16),
|
| 32 |
+
b: parseInt(result[3], 16)
|
| 33 |
+
} : { r: 0, g: 0, b: 0 };
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Save data to localStorage
|
| 37 |
+
function saveData() {
|
| 38 |
+
localStorage.setItem('sankeyData', JSON.stringify(sankeyData));
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
// Initialize the diagram
|
| 42 |
function initDiagram() {
|
| 43 |
const svg = d3.select("#sankey-diagram");
|
| 44 |
svg.selectAll("*").remove();
|
| 45 |
+
|
| 46 |
// Early return if SVG container doesn't exist
|
| 47 |
if (!svg.node()) return;
|
| 48 |
+
|
| 49 |
+
// Set up the sankey diagram properties
|
| 50 |
const width = document.getElementById('diagram-container').clientWidth;
|
| 51 |
const height = parseInt(document.getElementById('diagram-height').value);
|
| 52 |
+
const linkAlpha = parseFloat(document.getElementById('link-alpha').value);
|
| 53 |
|
| 54 |
const sankey = d3.sankey()
|
| 55 |
.nodeWidth(parseInt(document.getElementById('node-width').value))
|
| 56 |
.nodePadding(20)
|
| 57 |
.extent([[1, 1], [width - 1, height - 6]]);
|
| 58 |
+
|
| 59 |
// Generate the sankey diagram
|
| 60 |
const graph = sankey({
|
| 61 |
nodes: sankeyData.nodes.map(d => Object.assign({}, d)),
|
|
|
|
| 64 |
|
| 65 |
const nodes = graph.nodes;
|
| 66 |
const links = graph.links;
|
| 67 |
+
|
| 68 |
+
// Clear previous diagram
|
| 69 |
svg.attr("width", width)
|
| 70 |
.attr("height", height)
|
| 71 |
.attr("viewBox", [0, 0, width, height])
|
| 72 |
.attr("style", "max-width: 100%; height: auto;");
|
| 73 |
|
| 74 |
// Add links
|
| 75 |
+
const link = svg.append("g")
|
| 76 |
.attr("fill", "none")
|
|
|
|
| 77 |
.selectAll("path")
|
| 78 |
.data(links)
|
| 79 |
.join("path")
|
| 80 |
.attr("d", d3.sankeyLinkHorizontal())
|
| 81 |
+
.attr("stroke", d => {
|
| 82 |
+
const srcColor = sankeyData.nodes[d.source.index]?.color || "#93c5fd";
|
| 83 |
+
const rgb = hexToRgb(srcColor);
|
| 84 |
+
return `rgba(${rgb.r},${rgb.g},${rgb.b},${linkAlpha})`;
|
| 85 |
+
})
|
| 86 |
.attr("stroke-width", d => Math.max(1, d.width))
|
| 87 |
+
.style("mix-blend-mode", "multiply")
|
| 88 |
+
.on("mouseover", function() {
|
| 89 |
+
d3.select(this).attr("stroke-opacity", 0.8);
|
| 90 |
+
})
|
| 91 |
+
.on("mouseout", function() {
|
| 92 |
+
d3.select(this).attr("stroke-opacity", linkAlpha);
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
// Add nodes with better styling
|
| 96 |
const node = svg.append("g")
|
| 97 |
.selectAll("g")
|
|
|
|
| 108 |
node.append("rect")
|
| 109 |
.attr("height", d => d.y1 - d.y0)
|
| 110 |
.attr("width", d => d.x1 - d.x0)
|
| 111 |
+
.attr("fill", d => d.color || "#4f46e5")
|
| 112 |
.attr("stroke", "#fff")
|
| 113 |
.attr("stroke-width", 1)
|
| 114 |
.attr("rx", 3) // Rounded corners
|
| 115 |
.attr("ry", 3);
|
| 116 |
|
| 117 |
+
// Add drag behavior
|
| 118 |
+
node.call(d3.drag()
|
| 119 |
+
.subject(d => ({ x: d.x0, y: d.y0 }))
|
| 120 |
+
.on("start", function(event) {
|
| 121 |
+
d3.select(this).raise();
|
| 122 |
+
})
|
| 123 |
+
.on("drag", function(event, d) {
|
| 124 |
+
// Update node position
|
| 125 |
+
d.x0 = event.x;
|
| 126 |
+
d.x1 = event.x + (d.x1 - d.x0);
|
| 127 |
+
d.y0 = event.y;
|
| 128 |
+
d.y1 = event.y + (d.y1 - d.y0);
|
| 129 |
+
|
| 130 |
+
nodePositions[d.index] = { x: d.x0, y: d.y0 };
|
| 131 |
+
d3.select(this).attr("transform", `translate(${d.x0},${d.y0})`);
|
| 132 |
+
|
| 133 |
+
// Update links
|
| 134 |
+
link.attr("d", d3.sankeyLinkHorizontal());
|
| 135 |
+
})
|
| 136 |
+
);
|
| 137 |
|
| 138 |
// Add text with customizable position
|
| 139 |
const textPosition = document.getElementById('text-position').value;
|
|
|
|
| 161 |
.text(d => d.name)
|
| 162 |
.attr("fill", document.getElementById('text-color').value)
|
| 163 |
.attr("font-size", "12px")
|
| 164 |
+
.attr("font-weight", "bold")
|
| 165 |
+
.attr("pointer-events", "all")
|
| 166 |
+
.attr("cursor", "pointer")
|
| 167 |
+
.on("dblclick", function(event, d) {
|
| 168 |
+
const newName = prompt("Enter new node name:", d.name);
|
| 169 |
+
if (newName !== null && newName.trim() !== "") {
|
| 170 |
+
sankeyData.nodes[d.index].name = newName.trim();
|
| 171 |
+
updateLists();
|
| 172 |
+
initDiagram();
|
| 173 |
+
saveData();
|
| 174 |
+
}
|
| 175 |
+
});
|
| 176 |
}
|
| 177 |
|
| 178 |
// Update UI lists
|
|
|
|
| 186 |
nodeElement.className = 'flex items-center justify-between bg-white p-2 rounded border border-gray-200';
|
| 187 |
nodeElement.innerHTML = `
|
| 188 |
<span>${node.name}</span>
|
| 189 |
+
<div class="flex items-center space-x-2">
|
| 190 |
+
<input type="color" class="node-color-picker w-6 h-6" data-index="${index}" value="${node.color || '#4f46e5'}">
|
| 191 |
+
<button class="text-red-500 hover:text-red-700 delete-node" data-index="${index}">
|
| 192 |
+
<i data-feather="trash-2" width="16"></i>
|
| 193 |
+
</button>
|
| 194 |
+
</div>
|
| 195 |
`;
|
| 196 |
nodesList.appendChild(nodeElement);
|
| 197 |
});
|
|
|
|
| 208 |
linkElement.className = 'flex items-center justify-between bg-white p-2 rounded border border-gray-200';
|
| 209 |
linkElement.innerHTML = `
|
| 210 |
<span>${sourceNode} → ${targetNode} (${link.value})</span>
|
| 211 |
+
<button class="text-red-500 hover:text-red-700 delete-link" data-index="${index}">
|
| 212 |
<i data-feather="trash-2" width="16"></i>
|
| 213 |
</button>
|
| 214 |
`;
|
|
|
|
| 241 |
|
| 242 |
// Export to CSV function
|
| 243 |
function exportToCSV() {
|
| 244 |
+
if (!sankeyData.nodes.length || !sankeyData.links.length) {
|
| 245 |
+
alert("No data to export.");
|
| 246 |
+
return;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
let csvContent = "Source,Target,Value\r\n";
|
| 250 |
sankeyData.links.forEach(link => {
|
| 251 |
+
const src = sankeyData.nodes[link.source]?.name || "";
|
| 252 |
+
const tgt = sankeyData.nodes[link.target]?.name || "";
|
| 253 |
+
csvContent += `"${src}","${tgt}",${link.value}\r\n`;
|
| 254 |
});
|
| 255 |
+
|
| 256 |
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
| 257 |
const url = URL.createObjectURL(blob);
|
| 258 |
+
const a = document.createElement('a');
|
| 259 |
+
a.href = url;
|
| 260 |
+
a.download = "sankey_data.csv";
|
| 261 |
+
document.body.appendChild(a);
|
| 262 |
+
a.click();
|
| 263 |
+
document.body.removeChild(a);
|
| 264 |
+
URL.revokeObjectURL(url);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
// Import from CSV function
|
| 268 |
+
function importFromCSV(file) {
|
| 269 |
+
const reader = new FileReader();
|
| 270 |
+
reader.onload = function(e) {
|
| 271 |
+
const text = e.target.result;
|
| 272 |
+
const lines = text.split('\n').filter(line => line.trim() !== '');
|
| 273 |
+
|
| 274 |
+
if (lines.length < 2) {
|
| 275 |
+
alert("Invalid CSV format");
|
| 276 |
+
return;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// Reset data
|
| 280 |
+
sankeyData = { nodes: [], links: [] };
|
| 281 |
+
nodePositions = {};
|
| 282 |
+
|
| 283 |
+
// Parse header
|
| 284 |
+
const header = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
| 285 |
+
const sourceIdx = header.indexOf('Source');
|
| 286 |
+
const targetIdx = header.indexOf('Target');
|
| 287 |
+
const valueIdx = header.indexOf('Value');
|
| 288 |
+
|
| 289 |
+
if (sourceIdx === -1 || targetIdx === -1 || valueIdx === -1) {
|
| 290 |
+
alert("CSV must have columns: Source, Target, Value");
|
| 291 |
+
return;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// Node name to index mapping
|
| 295 |
+
const nodeMap = {};
|
| 296 |
+
let nodeIndex = 0;
|
| 297 |
+
|
| 298 |
+
// Parse data rows
|
| 299 |
+
for (let i = 1; i < lines.length; i++) {
|
| 300 |
+
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
|
| 301 |
+
if (values.length < 3) continue;
|
| 302 |
+
|
| 303 |
+
const sourceName = values[sourceIdx];
|
| 304 |
+
const targetName = values[targetIdx];
|
| 305 |
+
const value = parseFloat(values[valueIdx]);
|
| 306 |
+
|
| 307 |
+
if (isNaN(value)) continue;
|
| 308 |
+
|
| 309 |
+
// Add nodes if they don't exist
|
| 310 |
+
if (!(sourceName in nodeMap)) {
|
| 311 |
+
nodeMap[sourceName] = nodeIndex++;
|
| 312 |
+
sankeyData.nodes.push({ name: sourceName, color: "#4f46e5" });
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
if (!(targetName in nodeMap)) {
|
| 316 |
+
nodeMap[targetName] = nodeIndex++;
|
| 317 |
+
sankeyData.nodes.push({ name: targetName, color: "#4f46e5" });
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
// Add link
|
| 321 |
+
sankeyData.links.push({
|
| 322 |
+
source: nodeMap[sourceName],
|
| 323 |
+
target: nodeMap[targetName],
|
| 324 |
+
value: value
|
| 325 |
+
});
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
updateLists();
|
| 329 |
+
initDiagram();
|
| 330 |
+
saveData();
|
| 331 |
+
};
|
| 332 |
+
reader.readAsText(file);
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
// Center diagram
|
| 336 |
+
function centerDiagram() {
|
| 337 |
+
nodePositions = {};
|
| 338 |
+
initDiagram();
|
| 339 |
}
|
| 340 |
|
| 341 |
// Event Listeners
|
|
|
|
| 344 |
document.getElementById('add-node').addEventListener('click', () => {
|
| 345 |
const nodeName = document.getElementById('node-name').value.trim();
|
| 346 |
if (nodeName) {
|
| 347 |
+
sankeyData.nodes.push({ name: nodeName, color: "#4f46e5" });
|
| 348 |
document.getElementById('node-name').value = '';
|
| 349 |
updateLists();
|
| 350 |
initDiagram();
|
| 351 |
+
saveData();
|
| 352 |
}
|
| 353 |
});
|
| 354 |
+
|
| 355 |
// Add link
|
| 356 |
document.getElementById('add-link').addEventListener('click', () => {
|
| 357 |
const sourceIndex = parseInt(document.getElementById('source-node').value);
|
|
|
|
| 367 |
document.getElementById('link-value').value = '';
|
| 368 |
updateLists();
|
| 369 |
initDiagram();
|
| 370 |
+
saveData();
|
| 371 |
}
|
| 372 |
});
|
| 373 |
|
|
|
|
|
|
|
|
|
|
| 374 |
// Export to CSV
|
| 375 |
document.getElementById('export-csv').addEventListener('click', exportToCSV);
|
| 376 |
|
| 377 |
+
// Import from CSV
|
| 378 |
+
document.getElementById('import-csv').addEventListener('click', () => {
|
| 379 |
+
document.getElementById('csv-file').click();
|
| 380 |
+
});
|
| 381 |
+
|
| 382 |
+
document.getElementById('csv-file').addEventListener('change', (e) => {
|
| 383 |
+
const file = e.target.files[0];
|
| 384 |
+
if (file) {
|
| 385 |
+
importFromCSV(file);
|
| 386 |
+
}
|
| 387 |
+
e.target.value = ''; // Reset input
|
| 388 |
+
});
|
| 389 |
+
|
| 390 |
+
// Center diagram
|
| 391 |
+
document.getElementById('center-diagram').addEventListener('click', () => {
|
| 392 |
+
centerDiagram();
|
| 393 |
+
saveData();
|
| 394 |
+
});
|
| 395 |
+
|
| 396 |
+
// Delete node or link
|
| 397 |
document.addEventListener('click', (e) => {
|
| 398 |
+
if (e.target.closest('.delete-node')) {
|
| 399 |
+
const button = e.target.closest('.delete-node');
|
| 400 |
const index = parseInt(button.getAttribute('data-index'));
|
|
|
|
| 401 |
|
| 402 |
+
// Delete node and associated links
|
| 403 |
+
sankeyData.links = sankeyData.links.filter(link =>
|
| 404 |
+
link.source !== index && link.target !== index
|
| 405 |
+
);
|
| 406 |
+
sankeyData.nodes.splice(index, 1);
|
| 407 |
+
|
| 408 |
+
// Update link indices
|
| 409 |
+
sankeyData.links = sankeyData.links.map(link => ({
|
| 410 |
+
source: link.source > index ? link.source - 1 : link.source,
|
| 411 |
+
target: link.target > index ? link.target - 1 : link.target,
|
| 412 |
+
value: link.value
|
| 413 |
+
}));
|
| 414 |
+
|
| 415 |
+
// Remove node position data
|
| 416 |
+
delete nodePositions[index];
|
| 417 |
+
|
| 418 |
+
// Update remaining node positions
|
| 419 |
+
const newNodePositions = {};
|
| 420 |
+
Object.keys(nodePositions).forEach(key => {
|
| 421 |
+
const keyNum = parseInt(key);
|
| 422 |
+
if (keyNum > index) {
|
| 423 |
+
newNodePositions[keyNum - 1] = nodePositions[key];
|
| 424 |
+
} else if (keyNum < index) {
|
| 425 |
+
newNodePositions[keyNum] = nodePositions[key];
|
| 426 |
+
}
|
| 427 |
+
});
|
| 428 |
+
nodePositions = newNodePositions;
|
| 429 |
+
|
| 430 |
+
updateLists();
|
| 431 |
+
initDiagram();
|
| 432 |
+
saveData();
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
if (e.target.closest('.delete-link')) {
|
| 436 |
+
const button = e.target.closest('.delete-link');
|
| 437 |
+
const index = parseInt(button.getAttribute('data-index'));
|
| 438 |
+
|
| 439 |
+
sankeyData.links.splice(index, 1);
|
| 440 |
|
| 441 |
updateLists();
|
| 442 |
initDiagram();
|
| 443 |
+
saveData();
|
| 444 |
+
}
|
| 445 |
+
});
|
| 446 |
+
|
| 447 |
+
// Node color change
|
| 448 |
+
document.addEventListener('input', (e) => {
|
| 449 |
+
if (e.target.classList.contains('node-color-picker')) {
|
| 450 |
+
const index = parseInt(e.target.getAttribute('data-index'));
|
| 451 |
+
const color = e.target.value;
|
| 452 |
+
sankeyData.nodes[index].color = color;
|
| 453 |
+
initDiagram();
|
| 454 |
+
saveData();
|
| 455 |
}
|
| 456 |
});
|
| 457 |
|
| 458 |
// Color and options changes
|
|
|
|
|
|
|
| 459 |
document.getElementById('text-color').addEventListener('input', initDiagram);
|
| 460 |
document.getElementById('text-position').addEventListener('change', initDiagram);
|
| 461 |
document.getElementById('node-width').addEventListener('input', initDiagram);
|
| 462 |
document.getElementById('diagram-height').addEventListener('input', initDiagram);
|
| 463 |
+
document.getElementById('link-alpha').addEventListener('input', initDiagram);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
|
| 465 |
// Initialize dropdowns and lists
|
| 466 |
updateLists();
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
|
|
|
|
| 2 |
/* Custom styles for the sankey diagram */
|
| 3 |
#sankey-diagram {
|
| 4 |
width: 100%;
|
|
@@ -7,15 +8,15 @@
|
|
| 7 |
border-radius: 0.5rem;
|
| 8 |
}
|
| 9 |
#diagram-container {
|
| 10 |
-
background:
|
| 11 |
border-radius: 0.5rem;
|
| 12 |
-
margin: 0.5rem;
|
| 13 |
position: relative;
|
|
|
|
| 14 |
}
|
| 15 |
.node rect {
|
| 16 |
cursor: move;
|
| 17 |
fill-opacity: 0.9;
|
| 18 |
-
|
| 19 |
}
|
| 20 |
|
| 21 |
.node text {
|
|
|
|
| 1 |
|
| 2 |
+
|
| 3 |
/* Custom styles for the sankey diagram */
|
| 4 |
#sankey-diagram {
|
| 5 |
width: 100%;
|
|
|
|
| 8 |
border-radius: 0.5rem;
|
| 9 |
}
|
| 10 |
#diagram-container {
|
| 11 |
+
background: #f1f5f9;
|
| 12 |
border-radius: 0.5rem;
|
|
|
|
| 13 |
position: relative;
|
| 14 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 15 |
}
|
| 16 |
.node rect {
|
| 17 |
cursor: move;
|
| 18 |
fill-opacity: 0.9;
|
| 19 |
+
shape-rendering: crispEdges;
|
| 20 |
}
|
| 21 |
|
| 22 |
.node text {
|