Spaces:
Running
Running
Commit ·
76f3c6e
1
Parent(s): 7888a2c
Editar Links y Nodos
Browse files- index.html +92 -47
- script.js +192 -23
- style.css +67 -0
index.html
CHANGED
|
@@ -11,31 +11,78 @@
|
|
| 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 |
-
|
| 16 |
-
|
| 17 |
-
<div class="
|
| 18 |
-
<div class="
|
| 19 |
-
<
|
| 20 |
-
<button id="
|
| 21 |
-
<i data-feather="
|
| 22 |
</button>
|
| 23 |
</div>
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
<
|
| 30 |
-
</
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
<div>
|
| 33 |
<label class="block text-sm text-gray-600 mb-1">Text Color</label>
|
| 34 |
-
<input type="color" id="text-color" value="#000000" class="w-full h-
|
| 35 |
</div>
|
| 36 |
<div>
|
| 37 |
<label class="block text-sm text-gray-600 mb-1">Text Position</label>
|
| 38 |
-
<select id="text-position" class="w-full p-2 border rounded-md
|
| 39 |
<option value="left">Left</option>
|
| 40 |
<option value="middle" selected>Middle</option>
|
| 41 |
<option value="right">Right</option>
|
|
@@ -55,38 +102,36 @@
|
|
| 55 |
</div>
|
| 56 |
</div>
|
| 57 |
</div>
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
</button>
|
| 73 |
-
</div>
|
| 74 |
-
<input type="file" id="csv-file" accept=".csv" class="hidden">
|
| 75 |
-
</div>
|
| 76 |
-
|
| 77 |
-
<!-- Appearance moved into the Settings panel -->
|
| 78 |
|
| 79 |
<!-- Nodes Management -->
|
| 80 |
-
<div class="bg-gray-50
|
| 81 |
-
<
|
| 82 |
-
<
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
-
<div id="nodes-list" class="max-h-60 overflow-y-auto border rounded-md p-2 space-y-2"></div>
|
| 90 |
</div>
|
| 91 |
</div>
|
| 92 |
|
|
|
|
| 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 flex-col">
|
| 15 |
+
<!-- Edit Modal -->
|
| 16 |
+
<div id="edit-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden flex items-center justify-center z-50">
|
| 17 |
+
<div class="bg-white rounded-lg p-6 w-96 shadow-xl">
|
| 18 |
+
<div class="flex justify-between items-center mb-4">
|
| 19 |
+
<h3 id="modal-title" class="text-lg font-semibold text-gray-800"></h3>
|
| 20 |
+
<button id="close-modal" class="text-gray-500 hover:text-gray-700">
|
| 21 |
+
<i data-feather="x"></i>
|
| 22 |
</button>
|
| 23 |
</div>
|
| 24 |
+
<div id="modal-content">
|
| 25 |
+
<!-- Node editing form -->
|
| 26 |
+
<div id="node-edit-form" class="hidden">
|
| 27 |
+
<div class="mb-4">
|
| 28 |
+
<label class="block text-sm text-gray-600 mb-1">Node Name</label>
|
| 29 |
+
<input type="text" id="edit-node-name" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
<!-- Link editing form -->
|
| 33 |
+
<div id="link-edit-form" class="hidden">
|
| 34 |
+
<div class="mb-4">
|
| 35 |
+
<label class="block text-sm text-gray-600 mb-1">Source Node</label>
|
| 36 |
+
<select id="edit-link-source" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="mb-4">
|
| 39 |
+
<label class="block text-sm text-gray-600 mb-1">Target Node</label>
|
| 40 |
+
<select id="edit-link-target" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="mb-4">
|
| 43 |
+
<label class="block text-sm text-gray-600 mb-1">Value</label>
|
| 44 |
+
<input type="number" id="edit-link-value" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" min="0">
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
<div class="flex justify-end space-x-2 mt-6">
|
| 49 |
+
<button id="cancel-edit" class="px-4 py-2 text-gray-600 hover:text-gray-800 rounded-md">Cancel</button>
|
| 50 |
+
<button id="save-edit" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md">Save</button>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
<!-- Main Header -->
|
| 55 |
+
<div class="bg-white shadow-lg p-4 flex items-center justify-between relative">
|
| 56 |
+
<div class="flex items-center">
|
| 57 |
+
<button id="toggle-sidebar" class="text-gray-600 hover:text-gray-800 p-2 rounded-md mr-4 tooltip-trigger" title="Toggle Sidebar">
|
| 58 |
+
<i data-feather="menu"></i>
|
| 59 |
+
</button>
|
| 60 |
+
<h1 class="text-xl font-bold text-gray-800">Sankey Flow Wizard</h1>
|
| 61 |
+
</div>
|
| 62 |
+
<div class="flex items-center space-x-2">
|
| 63 |
+
<button id="import-csv" class="p-2 text-gray-600 hover:text-gray-800 rounded-md tooltip-trigger" title="Import CSV">
|
| 64 |
+
<i data-feather="upload"></i>
|
| 65 |
+
</button>
|
| 66 |
+
<button id="export-csv" class="p-2 text-gray-600 hover:text-gray-800 rounded-md tooltip-trigger" title="Export CSV">
|
| 67 |
+
<i data-feather="download"></i>
|
| 68 |
+
</button>
|
| 69 |
+
<button id="center-diagram" class="p-2 text-gray-600 hover:text-gray-800 rounded-md tooltip-trigger" title="Center Diagram">
|
| 70 |
+
<i data-feather="crosshair"></i>
|
| 71 |
+
</button>
|
| 72 |
+
<div class="relative">
|
| 73 |
+
<button id="settings-btn" class="p-2 text-gray-600 hover:text-gray-800 rounded-md tooltip-trigger" title="Settings">
|
| 74 |
+
<i data-feather="settings"></i>
|
| 75 |
+
</button>
|
| 76 |
+
<!-- Settings Dropdown Menu -->
|
| 77 |
+
<div id="settings-dropdown" class="hidden absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg z-50">
|
| 78 |
+
<div class="p-4 space-y-4">
|
| 79 |
<div>
|
| 80 |
<label class="block text-sm text-gray-600 mb-1">Text Color</label>
|
| 81 |
+
<input type="color" id="text-color" value="#000000" class="w-full h-8">
|
| 82 |
</div>
|
| 83 |
<div>
|
| 84 |
<label class="block text-sm text-gray-600 mb-1">Text Position</label>
|
| 85 |
+
<select id="text-position" class="w-full p-2 border rounded-md">
|
| 86 |
<option value="left">Left</option>
|
| 87 |
<option value="middle" selected>Middle</option>
|
| 88 |
<option value="right">Right</option>
|
|
|
|
| 102 |
</div>
|
| 103 |
</div>
|
| 104 |
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="flex flex-1">
|
| 109 |
+
<!-- Sidebar -->
|
| 110 |
+
<div id="sidebar" class="w-80 bg-white shadow-lg flex flex-col transition-all duration-300 ease-in-out">
|
| 111 |
+
<div class="p-2 border-b">
|
| 112 |
+
<h2 class="text-lg font-semibold text-gray-800">Diagram Editor</h2>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
| 116 |
+
|
| 117 |
+
<!-- File input for CSV import -->
|
| 118 |
+
<input type="file" id="csv-file" accept=".csv" class="hidden">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
<!-- Nodes Management -->
|
| 121 |
+
<div class="bg-gray-50 rounded-lg">
|
| 122 |
+
<div class="p-2 border-b">
|
| 123 |
+
<h3 class="font-medium text-gray-700 flex items-center">
|
| 124 |
+
<i data-feather="circle" class="mr-2"></i> Nodes
|
| 125 |
+
</h3>
|
| 126 |
+
</div>
|
| 127 |
+
<div class="p-3">
|
| 128 |
+
<div class="space-y-3">
|
| 129 |
+
<div id="nodes-list" class="max-h-[200px] overflow-y-auto border rounded-md p-2 space-y-2 bg-white"></div>
|
| 130 |
+
<div class="flex space-x-2">
|
| 131 |
+
<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">
|
| 132 |
+
<button id="add-node" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition">Add</button>
|
| 133 |
+
</div>
|
| 134 |
</div>
|
|
|
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
|
script.js
CHANGED
|
@@ -1,4 +1,109 @@
|
|
| 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
// Define the d3.sankey layout function
|
| 3 |
d3.sankey = function() {
|
| 4 |
var sankey = {},
|
|
@@ -320,29 +425,81 @@ function saveData() {
|
|
| 320 |
localStorage.setItem('sankeyData', JSON.stringify(sankeyData));
|
| 321 |
}
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
// Initialize the diagram
|
| 324 |
function initDiagram() {
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
const graph = {
|
| 347 |
nodes: sankeyData.nodes.map(d => Object.assign({}, d)),
|
| 348 |
links: sankeyData.links.map(d => Object.assign({}, d))
|
|
@@ -505,7 +662,7 @@ function updateLists() {
|
|
| 505 |
|
| 506 |
sankeyData.nodes.forEach((node, index) => {
|
| 507 |
const nodeElement = document.createElement('div');
|
| 508 |
-
nodeElement.className = 'flex items-center justify-between bg-white p-2 rounded border border-gray-200';
|
| 509 |
nodeElement.innerHTML = `
|
| 510 |
<span>${node.name}</span>
|
| 511 |
<div class="flex items-center space-x-2">
|
|
@@ -515,6 +672,12 @@ function updateLists() {
|
|
| 515 |
</button>
|
| 516 |
</div>
|
| 517 |
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
nodesList.appendChild(nodeElement);
|
| 519 |
});
|
| 520 |
|
|
@@ -527,13 +690,19 @@ function updateLists() {
|
|
| 527 |
const targetNode = sankeyData.nodes[link.target]?.name || 'Unknown';
|
| 528 |
|
| 529 |
const linkElement = document.createElement('div');
|
| 530 |
-
linkElement.className = 'flex items-center justify-between bg-white p-2 rounded border border-gray-200';
|
| 531 |
linkElement.innerHTML = `
|
| 532 |
<span>${sourceNode} → ${targetNode} (${link.value})</span>
|
| 533 |
<button class="text-red-500 hover:text-red-700 delete-link" data-index="${index}">
|
| 534 |
<i data-feather="trash-2" width="16"></i>
|
| 535 |
</button>
|
| 536 |
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
linksList.appendChild(linkElement);
|
| 538 |
});
|
| 539 |
|
|
|
|
| 1 |
|
| 2 |
+
// DOM Elements
|
| 3 |
+
const sidebar = document.getElementById('sidebar');
|
| 4 |
+
const toggleSidebarBtn = document.getElementById('toggle-sidebar');
|
| 5 |
+
const settingsBtn = document.getElementById('settings-btn');
|
| 6 |
+
const settingsDropdown = document.getElementById('settings-dropdown');
|
| 7 |
+
|
| 8 |
+
// Modal Elements
|
| 9 |
+
const editModal = document.getElementById('edit-modal');
|
| 10 |
+
const modalTitle = document.getElementById('modal-title');
|
| 11 |
+
const nodeEditForm = document.getElementById('node-edit-form');
|
| 12 |
+
const linkEditForm = document.getElementById('link-edit-form');
|
| 13 |
+
const editNodeName = document.getElementById('edit-node-name');
|
| 14 |
+
const editLinkSource = document.getElementById('edit-link-source');
|
| 15 |
+
const editLinkTarget = document.getElementById('edit-link-target');
|
| 16 |
+
const editLinkValue = document.getElementById('edit-link-value');
|
| 17 |
+
const saveEditBtn = document.getElementById('save-edit');
|
| 18 |
+
const cancelEditBtn = document.getElementById('cancel-edit');
|
| 19 |
+
const closeModalBtn = document.getElementById('close-modal');
|
| 20 |
+
|
| 21 |
+
let currentEditItem = null; // Store the item being edited
|
| 22 |
+
|
| 23 |
+
// Modal Functions
|
| 24 |
+
function openModal() {
|
| 25 |
+
editModal.classList.remove('hidden');
|
| 26 |
+
feather.replace();
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
function closeModal() {
|
| 30 |
+
editModal.classList.add('hidden');
|
| 31 |
+
currentEditItem = null;
|
| 32 |
+
nodeEditForm.classList.add('hidden');
|
| 33 |
+
linkEditForm.classList.add('hidden');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
function populateNodeForm(node) {
|
| 37 |
+
modalTitle.textContent = 'Edit Node';
|
| 38 |
+
nodeEditForm.classList.remove('hidden');
|
| 39 |
+
linkEditForm.classList.add('hidden');
|
| 40 |
+
editNodeName.value = node.name;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function populateLinkForm(link) {
|
| 44 |
+
modalTitle.textContent = 'Edit Link';
|
| 45 |
+
linkEditForm.classList.remove('hidden');
|
| 46 |
+
nodeEditForm.classList.add('hidden');
|
| 47 |
+
|
| 48 |
+
// Clear and populate source/target dropdowns
|
| 49 |
+
editLinkSource.innerHTML = '';
|
| 50 |
+
editLinkTarget.innerHTML = '';
|
| 51 |
+
sankeyData.nodes.forEach(node => {
|
| 52 |
+
const sourceOption = document.createElement('option');
|
| 53 |
+
sourceOption.value = node.name;
|
| 54 |
+
sourceOption.textContent = node.name;
|
| 55 |
+
editLinkSource.appendChild(sourceOption.cloneNode(true));
|
| 56 |
+
editLinkTarget.appendChild(sourceOption);
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
// Set current values
|
| 60 |
+
editLinkSource.value = sankeyData.nodes[link.source].name;
|
| 61 |
+
editLinkTarget.value = sankeyData.nodes[link.target].name;
|
| 62 |
+
editLinkValue.value = link.value;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Event Handlers for Modal
|
| 66 |
+
closeModalBtn.addEventListener('click', closeModal);
|
| 67 |
+
cancelEditBtn.addEventListener('click', closeModal);
|
| 68 |
+
|
| 69 |
+
// Click outside modal to close
|
| 70 |
+
editModal.addEventListener('click', (e) => {
|
| 71 |
+
if (e.target === editModal) {
|
| 72 |
+
closeModal();
|
| 73 |
+
}
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
// Toggle sidebar
|
| 77 |
+
toggleSidebarBtn.addEventListener('click', () => {
|
| 78 |
+
sidebar.classList.toggle('collapsed');
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
// Toggle settings dropdown
|
| 82 |
+
let isSettingsOpen = false;
|
| 83 |
+
settingsBtn.addEventListener('click', (e) => {
|
| 84 |
+
e.stopPropagation();
|
| 85 |
+
isSettingsOpen = !isSettingsOpen;
|
| 86 |
+
settingsDropdown.classList.toggle('hidden', !isSettingsOpen);
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
// Close settings dropdown when clicking outside
|
| 90 |
+
document.addEventListener('click', (e) => {
|
| 91 |
+
if (!settingsDropdown.contains(e.target) && !settingsBtn.contains(e.target)) {
|
| 92 |
+
settingsDropdown.classList.add('hidden');
|
| 93 |
+
isSettingsOpen = false;
|
| 94 |
+
}
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
// Prevent settings dropdown from closing when clicking inside it
|
| 98 |
+
settingsDropdown.addEventListener('click', (e) => {
|
| 99 |
+
e.stopPropagation();
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
// Initialize Feather icons
|
| 103 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 104 |
+
feather.replace();
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
// Define the d3.sankey layout function
|
| 108 |
d3.sankey = function() {
|
| 109 |
var sankey = {},
|
|
|
|
| 425 |
localStorage.setItem('sankeyData', JSON.stringify(sankeyData));
|
| 426 |
}
|
| 427 |
|
| 428 |
+
// Edit node or link
|
| 429 |
+
function handleEditNode(node, index) {
|
| 430 |
+
currentEditItem = { type: 'node', index };
|
| 431 |
+
populateNodeForm(node);
|
| 432 |
+
openModal();
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
function handleEditLink(link, index) {
|
| 436 |
+
currentEditItem = { type: 'link', index };
|
| 437 |
+
populateLinkForm(link);
|
| 438 |
+
openModal();
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
// Save changes from modal
|
| 442 |
+
saveEditBtn.addEventListener('click', () => {
|
| 443 |
+
if (currentEditItem) {
|
| 444 |
+
if (currentEditItem.type === 'node') {
|
| 445 |
+
const newName = editNodeName.value.trim();
|
| 446 |
+
if (newName) {
|
| 447 |
+
sankeyData.nodes[currentEditItem.index].name = newName;
|
| 448 |
+
}
|
| 449 |
+
} else if (currentEditItem.type === 'link') {
|
| 450 |
+
const sourceNode = editLinkSource.value;
|
| 451 |
+
const targetNode = editLinkTarget.value;
|
| 452 |
+
const value = parseInt(editLinkValue.value);
|
| 453 |
+
|
| 454 |
+
if (sourceNode && targetNode && value > 0) {
|
| 455 |
+
sankeyData.links[currentEditItem.index] = {
|
| 456 |
+
source: sankeyData.nodes.findIndex(n => n.name === sourceNode),
|
| 457 |
+
target: sankeyData.nodes.findIndex(n => n.name === targetNode),
|
| 458 |
+
value: value
|
| 459 |
+
};
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
updateLists();
|
| 464 |
+
initDiagram();
|
| 465 |
+
saveData();
|
| 466 |
+
closeModal();
|
| 467 |
+
}
|
| 468 |
+
});
|
| 469 |
+
|
| 470 |
// Initialize the diagram
|
| 471 |
function initDiagram() {
|
| 472 |
+
const svg = d3.select("#sankey-diagram");
|
| 473 |
+
svg.selectAll("*").remove();
|
| 474 |
+
|
| 475 |
+
// Early return if SVG container doesn't exist
|
| 476 |
+
if (!svg.node()) return;
|
| 477 |
+
|
| 478 |
+
// Get the container dimensions
|
| 479 |
+
const container = document.getElementById('diagram-container');
|
| 480 |
+
const width = container.clientWidth;
|
| 481 |
+
const containerHeight = container.clientHeight;
|
| 482 |
+
|
| 483 |
+
// Set up the sankey diagram properties
|
| 484 |
+
const baseHeight = parseInt(document.getElementById('diagram-height').value);
|
| 485 |
+
const textPosition = document.getElementById('text-position').value;
|
| 486 |
+
const textPadding = textPosition !== 'middle' ? 50 : 0; // Extra space for text above/below
|
| 487 |
+
|
| 488 |
+
// Use the larger of container height or base height to prevent shrinking
|
| 489 |
+
const height = Math.max(containerHeight, baseHeight + textPadding);
|
| 490 |
+
const linkAlpha = parseFloat(document.getElementById('link-alpha').value);
|
| 491 |
+
|
| 492 |
+
// Update SVG dimensions
|
| 493 |
+
svg.attr("width", width)
|
| 494 |
+
.attr("height", height)
|
| 495 |
+
.attr("viewBox", [0, 0, width, height])
|
| 496 |
+
.attr("preserveAspectRatio", "xMinYMin");
|
| 497 |
+
|
| 498 |
+
// Create Sankey generator with increased padding for better distribution
|
| 499 |
+
const sankey = d3.sankey()
|
| 500 |
+
.nodeWidth(parseInt(document.getElementById('node-width').value))
|
| 501 |
+
.nodePadding(120) // Increased padding for better vertical distribution
|
| 502 |
+
.size([width - textPadding, height - textPadding]); // Create a copy of the data
|
| 503 |
const graph = {
|
| 504 |
nodes: sankeyData.nodes.map(d => Object.assign({}, d)),
|
| 505 |
links: sankeyData.links.map(d => Object.assign({}, d))
|
|
|
|
| 662 |
|
| 663 |
sankeyData.nodes.forEach((node, index) => {
|
| 664 |
const nodeElement = document.createElement('div');
|
| 665 |
+
nodeElement.className = 'flex items-center justify-between bg-white p-2 rounded border border-gray-200 cursor-pointer';
|
| 666 |
nodeElement.innerHTML = `
|
| 667 |
<span>${node.name}</span>
|
| 668 |
<div class="flex items-center space-x-2">
|
|
|
|
| 672 |
</button>
|
| 673 |
</div>
|
| 674 |
`;
|
| 675 |
+
// Add double-click handler for editing
|
| 676 |
+
nodeElement.addEventListener('dblclick', (e) => {
|
| 677 |
+
if (!e.target.classList.contains('node-color-picker') && !e.target.closest('.delete-node')) {
|
| 678 |
+
handleEditNode(node, index);
|
| 679 |
+
}
|
| 680 |
+
});
|
| 681 |
nodesList.appendChild(nodeElement);
|
| 682 |
});
|
| 683 |
|
|
|
|
| 690 |
const targetNode = sankeyData.nodes[link.target]?.name || 'Unknown';
|
| 691 |
|
| 692 |
const linkElement = document.createElement('div');
|
| 693 |
+
linkElement.className = 'flex items-center justify-between bg-white p-2 rounded border border-gray-200 cursor-pointer';
|
| 694 |
linkElement.innerHTML = `
|
| 695 |
<span>${sourceNode} → ${targetNode} (${link.value})</span>
|
| 696 |
<button class="text-red-500 hover:text-red-700 delete-link" data-index="${index}">
|
| 697 |
<i data-feather="trash-2" width="16"></i>
|
| 698 |
</button>
|
| 699 |
`;
|
| 700 |
+
// Add double-click handler for editing
|
| 701 |
+
linkElement.addEventListener('dblclick', (e) => {
|
| 702 |
+
if (!e.target.closest('.delete-link')) {
|
| 703 |
+
handleEditLink(link, index);
|
| 704 |
+
}
|
| 705 |
+
});
|
| 706 |
linksList.appendChild(linkElement);
|
| 707 |
});
|
| 708 |
|
style.css
CHANGED
|
@@ -1,4 +1,68 @@
|
|
| 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
/* Custom styles for the sankey diagram */
|
| 4 |
#sankey-diagram {
|
|
@@ -12,6 +76,9 @@
|
|
| 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;
|
|
|
|
| 1 |
|
| 2 |
+
/* Main layout */
|
| 3 |
+
body {
|
| 4 |
+
display: flex;
|
| 5 |
+
flex-direction: column;
|
| 6 |
+
min-height: 100vh;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
/* Header styles */
|
| 10 |
+
.header {
|
| 11 |
+
background-color: white;
|
| 12 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 13 |
+
z-index: 10;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/* Tooltip styles */
|
| 17 |
+
.tooltip-trigger {
|
| 18 |
+
position: relative;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.tooltip-trigger:hover::after {
|
| 22 |
+
content: attr(title);
|
| 23 |
+
position: absolute;
|
| 24 |
+
bottom: -30px;
|
| 25 |
+
left: 50%;
|
| 26 |
+
transform: translateX(-50%);
|
| 27 |
+
padding: 4px 8px;
|
| 28 |
+
background-color: rgba(0, 0, 0, 0.8);
|
| 29 |
+
color: white;
|
| 30 |
+
border-radius: 4px;
|
| 31 |
+
font-size: 12px;
|
| 32 |
+
white-space: nowrap;
|
| 33 |
+
z-index: 1000;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Settings dropdown */
|
| 37 |
+
#settings-dropdown {
|
| 38 |
+
border: 1px solid #e5e7eb;
|
| 39 |
+
animation: fadeIn 0.2s ease-out;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
@keyframes fadeIn {
|
| 43 |
+
from { opacity: 0; transform: translateY(-10px); }
|
| 44 |
+
to { opacity: 1; transform: translateY(0); }
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* Sidebar transitions */
|
| 48 |
+
#sidebar {
|
| 49 |
+
transition: width 0.3s ease-in-out;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
#sidebar.collapsed {
|
| 53 |
+
width: 0;
|
| 54 |
+
overflow: hidden;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Button hover effects */
|
| 58 |
+
button {
|
| 59 |
+
transition: all 0.2s ease-in-out;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
button:hover {
|
| 63 |
+
transform: scale(1.05);
|
| 64 |
+
background-color: rgba(0, 0, 0, 0.05);
|
| 65 |
+
}
|
| 66 |
|
| 67 |
/* Custom styles for the sankey diagram */
|
| 68 |
#sankey-diagram {
|
|
|
|
| 76 |
border-radius: 0.5rem;
|
| 77 |
position: relative;
|
| 78 |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 79 |
+
min-height: 400px; /* Set a minimum height */
|
| 80 |
+
height: 100%; /* Take full height of parent */
|
| 81 |
+
display: flex; /* Ensure SVG fills container */
|
| 82 |
}
|
| 83 |
.node rect {
|
| 84 |
cursor: move;
|