Spaces:
Running
Running
| /** | |
| * NodeGrouping - Gom Nhóm Node Theo OpType | |
| * Cung cấp toggle để gom nhóm các node cùng opType thành compound nodes trong Cytoscape. | |
| * Requirements: 32.1, 32.2, 32.3, 32.4, 32.5 | |
| */ | |
| const NodeGrouping = (function () { | |
| 'use strict'; | |
| class NodeGrouping { | |
| constructor() { | |
| /** @type {boolean} */ | |
| this._enabled = false; | |
| /** @type {HTMLButtonElement|null} */ | |
| this._toggleBtn = null; | |
| /** @type {Set<string>} IDs of compound parent nodes we created */ | |
| this._groupIds = new Set(); | |
| /** @type {Set<string>} IDs of collapsed groups */ | |
| this._collapsedGroups = new Set(); | |
| /** @type {Function|null} */ | |
| this._unsubscribeGraphRendered = null; | |
| /** @type {Function|null} */ | |
| this._compoundTapHandler = null; | |
| /** @type {boolean} */ | |
| this._initialized = false; | |
| } | |
| // ─── Public API ───────────────────────────────────────────────────── | |
| /** | |
| * Khởi tạo: tạo toggle button, lắng nghe graph:rendered. | |
| */ | |
| init() { | |
| if (this._initialized) return; | |
| this._createToggleButton(); | |
| if (window.EventBus) { | |
| this._unsubscribeGraphRendered = window.EventBus.on( | |
| CONFIG.EVENTS.GRAPH_RENDERED, | |
| this._onGraphRendered.bind(this) | |
| ); | |
| } | |
| this._initialized = true; | |
| console.log('[NodeGrouping] Initialized'); | |
| } | |
| /** | |
| * Bật chế độ gom nhóm theo opType. | |
| */ | |
| enableGrouping() { | |
| var cy = this._getCytoscapeInstance(); | |
| if (!cy) { | |
| console.warn('[NodeGrouping] Cytoscape instance not available'); | |
| return; | |
| } | |
| // Collect op-nodes grouped by opType | |
| var groups = {}; | |
| cy.nodes().forEach(function (node) { | |
| var opType = node.data('opType'); | |
| if (!opType) return; | |
| // Skip nodes that are already compound parents | |
| if (node.hasClass('group-parent')) return; | |
| if (!groups[opType]) { | |
| groups[opType] = []; | |
| } | |
| groups[opType].push(node); | |
| }); | |
| // Create compound parent for each opType group | |
| var opTypes = Object.keys(groups); | |
| for (var i = 0; i < opTypes.length; i++) { | |
| var opType = opTypes[i]; | |
| var nodes = groups[opType]; | |
| var groupId = '_group_' + opType; | |
| // Add compound parent node | |
| cy.add({ | |
| group: 'nodes', | |
| data: { | |
| id: groupId, | |
| label: opType + ' (' + nodes.length + ')', | |
| opType: opType, | |
| isGroup: true, | |
| childCount: nodes.length | |
| }, | |
| classes: 'group-parent' | |
| }); | |
| this._groupIds.add(groupId); | |
| // Move child nodes into the compound parent | |
| for (var j = 0; j < nodes.length; j++) { | |
| nodes[j].move({ parent: groupId }); | |
| } | |
| } | |
| this._enabled = true; | |
| this._collapsedGroups.clear(); | |
| // Bind compound node click handler | |
| this._bindCompoundTap(cy); | |
| // Re-layout | |
| this._runLayout(cy); | |
| // Update button state | |
| this._updateToggleButton(); | |
| console.log('[NodeGrouping] Grouping enabled with', opTypes.length, 'groups'); | |
| } | |
| /** | |
| * Tắt chế độ gom nhóm, trả nodes về trạng thái độc lập. | |
| */ | |
| disableGrouping() { | |
| var cy = this._getCytoscapeInstance(); | |
| if (!cy) return; | |
| // Unbind compound tap handler | |
| this._unbindCompoundTap(cy); | |
| // Restore collapsed groups first (show hidden children) | |
| this._collapsedGroups.forEach(function (groupId) { | |
| var parent = cy.getElementById(groupId); | |
| if (parent && parent.length > 0) { | |
| parent.children().style('display', 'element'); | |
| } | |
| }); | |
| // Move all children out of compound parents | |
| this._groupIds.forEach(function (groupId) { | |
| var parent = cy.getElementById(groupId); | |
| if (parent && parent.length > 0) { | |
| parent.children().move({ parent: null }); | |
| } | |
| }); | |
| // Remove compound parent nodes | |
| this._groupIds.forEach(function (groupId) { | |
| var parent = cy.getElementById(groupId); | |
| if (parent && parent.length > 0) { | |
| cy.remove(parent); | |
| } | |
| }); | |
| this._groupIds.clear(); | |
| this._collapsedGroups.clear(); | |
| this._enabled = false; | |
| // Re-layout | |
| this._runLayout(cy); | |
| // Update button state | |
| this._updateToggleButton(); | |
| console.log('[NodeGrouping] Grouping disabled'); | |
| } | |
| /** | |
| * Expand một compound group (hiển thị children). | |
| * @param {string} groupId | |
| */ | |
| expandGroup(groupId) { | |
| var cy = this._getCytoscapeInstance(); | |
| if (!cy) return; | |
| var parent = cy.getElementById(groupId); | |
| if (!parent || parent.length === 0) return; | |
| parent.children().style('display', 'element'); | |
| parent.removeClass('collapsed'); | |
| this._collapsedGroups.delete(groupId); | |
| this._runLayout(cy); | |
| } | |
| /** | |
| * Collapse một compound group (ẩn children, chỉ hiển thị parent). | |
| * @param {string} groupId | |
| */ | |
| collapseGroup(groupId) { | |
| var cy = this._getCytoscapeInstance(); | |
| if (!cy) return; | |
| var parent = cy.getElementById(groupId); | |
| if (!parent || parent.length === 0) return; | |
| parent.children().style('display', 'none'); | |
| parent.addClass('collapsed'); | |
| this._collapsedGroups.add(groupId); | |
| this._runLayout(cy); | |
| } | |
| /** | |
| * Kiểm tra chế độ gom nhóm có đang bật không. | |
| * @returns {boolean} | |
| */ | |
| isEnabled() { | |
| return this._enabled; | |
| } | |
| /** | |
| * Hủy và dọn dẹp. | |
| */ | |
| destroy() { | |
| if (this._enabled) { | |
| this.disableGrouping(); | |
| } | |
| if (this._unsubscribeGraphRendered) { | |
| this._unsubscribeGraphRendered(); | |
| this._unsubscribeGraphRendered = null; | |
| } | |
| if (this._toggleBtn && this._toggleBtn.parentNode) { | |
| this._toggleBtn.parentNode.removeChild(this._toggleBtn); | |
| } | |
| this._toggleBtn = null; | |
| this._initialized = false; | |
| } | |
| // ─── Private ──────────────────────────────────────────────────────── | |
| /** | |
| * Create the toggle button and insert into the graph card header. | |
| */ | |
| _createToggleButton() { | |
| var exportContainer = document.getElementById('graphExportContainer'); | |
| if (!exportContainer) { | |
| console.warn('[NodeGrouping] #graphExportContainer not found'); | |
| return; | |
| } | |
| var btn = document.createElement('button'); | |
| btn.className = 'btn btn-outline-secondary btn-sm me-2'; | |
| btn.id = 'nodeGroupingBtn'; | |
| btn.title = 'Group by OpType'; | |
| btn.type = 'button'; | |
| btn.innerHTML = '<i class="fas fa-layer-group me-1"></i>Group'; | |
| var self = this; | |
| btn.addEventListener('click', function () { | |
| if (self._enabled) { | |
| self.disableGrouping(); | |
| } else { | |
| self.enableGrouping(); | |
| } | |
| }); | |
| // Insert at the beginning of the export container | |
| exportContainer.insertBefore(btn, exportContainer.firstChild); | |
| this._toggleBtn = btn; | |
| } | |
| /** | |
| * Update toggle button appearance based on enabled state. | |
| */ | |
| _updateToggleButton() { | |
| if (!this._toggleBtn) return; | |
| if (this._enabled) { | |
| this._toggleBtn.classList.remove('btn-outline-secondary'); | |
| this._toggleBtn.classList.add('btn-secondary'); | |
| this._toggleBtn.innerHTML = '<i class="fas fa-layer-group me-1"></i>Ungroup'; | |
| this._toggleBtn.title = 'Disable grouping'; | |
| } else { | |
| this._toggleBtn.classList.remove('btn-secondary'); | |
| this._toggleBtn.classList.add('btn-outline-secondary'); | |
| this._toggleBtn.innerHTML = '<i class="fas fa-layer-group me-1"></i>Group'; | |
| this._toggleBtn.title = 'Group by OpType'; | |
| } | |
| } | |
| /** | |
| * Bind tap handler on compound nodes for expand/collapse. | |
| * @param {object} cy - Cytoscape instance | |
| */ | |
| _bindCompoundTap(cy) { | |
| var self = this; | |
| this._compoundTapHandler = function (evt) { | |
| var node = evt.target; | |
| if (!node.data('isGroup')) return; | |
| var groupId = node.id(); | |
| if (self._collapsedGroups.has(groupId)) { | |
| self.expandGroup(groupId); | |
| } else { | |
| self.collapseGroup(groupId); | |
| } | |
| }; | |
| cy.on('tap', 'node.group-parent', this._compoundTapHandler); | |
| } | |
| /** | |
| * Unbind compound tap handler. | |
| * @param {object} cy - Cytoscape instance | |
| */ | |
| _unbindCompoundTap(cy) { | |
| if (this._compoundTapHandler) { | |
| cy.off('tap', 'node.group-parent', this._compoundTapHandler); | |
| this._compoundTapHandler = null; | |
| } | |
| } | |
| /** | |
| * Run layout on the graph after grouping changes. | |
| * @param {object} cy - Cytoscape instance | |
| */ | |
| _runLayout(cy) { | |
| var layout = { | |
| name: 'breadthfirst', | |
| directed: true, | |
| padding: 20, | |
| spacingFactor: 1.2, | |
| avoidOverlap: true, | |
| nodeDimensionsIncludeLabels: true, | |
| animate: true, | |
| animationDuration: 400 | |
| }; | |
| cy.layout(layout).run(); | |
| } | |
| /** | |
| * Handle graph:rendered event - reset grouping state for new graph. | |
| */ | |
| _onGraphRendered() { | |
| // Reset grouping state when a new graph is rendered | |
| this._groupIds.clear(); | |
| this._collapsedGroups.clear(); | |
| this._enabled = false; | |
| this._updateToggleButton(); | |
| } | |
| /** | |
| * Get the Cytoscape instance from the app's GraphVisualizer. | |
| * @returns {object|null} | |
| */ | |
| _getCytoscapeInstance() { | |
| try { | |
| if (window._onnxApp && typeof window._onnxApp.getGraphVisualizer === 'function') { | |
| var visualizer = window._onnxApp.getGraphVisualizer(); | |
| if (visualizer && visualizer._cy) { | |
| return visualizer._cy; | |
| } | |
| } | |
| } catch (err) { | |
| console.warn('[NodeGrouping] Could not access Cytoscape instance:', err); | |
| } | |
| return null; | |
| } | |
| } | |
| return NodeGrouping; | |
| })(); | |
| // Export as global for browser usage | |
| window.NodeGrouping = NodeGrouping; | |