model-explorer / js /ui /nodeGrouping.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* 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;