/** * TensorShapeInspector - Hiển thị thông tin shape tensor trên edge * Shows a tooltip with tensor name, shape, and data type when hovering/clicking on graph edges. * Looks up shape info from parsedModel inputs, outputs, and graph.valueInfo. * Requirements: 21.1, 21.2, 21.3, 21.4, 21.5 */ class TensorShapeInspector { /** * @param {Object} [options] * @param {Object} [options.cy] - Cytoscape instance to attach events to * @param {Object} [options.parsedModel] - Parsed ONNX model data */ constructor(options = {}) { /** @type {Object|null} Cytoscape instance */ this._cy = options.cy || null; /** @type {Object|null} Parsed model data */ this._parsedModel = options.parsedModel || null; /** @type {HTMLElement|null} Tooltip element */ this._tooltip = null; /** @type {Map} Cached tensor info lookup */ this._tensorInfoMap = new Map(); this._createTooltip(); if (this._parsedModel) { this._buildTensorInfoMap(this._parsedModel); } if (this._cy) { this._bindCytoscapeEvents(this._cy); } } // ─── Public API ──────────────────────────────────────────────────────────── /** * Attach to a Cytoscape instance for edge hover/click events. * @param {Object} cy - Cytoscape core instance */ attachToCytoscape(cy) { if (this._cy) { this._unbindCytoscapeEvents(this._cy); } this._cy = cy; if (cy) { this._bindCytoscapeEvents(cy); } } /** * Update the parsed model data used for tensor shape lookups. * @param {Object} parsedModel */ updateModel(parsedModel) { this._parsedModel = parsedModel; this._tensorInfoMap.clear(); if (parsedModel) { this._buildTensorInfoMap(parsedModel); } } /** * Look up tensor info by name. * @param {string} tensorName * @returns {{ name: string, shape: Array, dataType: string }|null} */ lookupTensor(tensorName) { if (!tensorName) return null; return this._tensorInfoMap.get(tensorName) || null; } /** * Destroy the inspector and clean up resources. */ destroy() { if (this._cy) { this._unbindCytoscapeEvents(this._cy); this._cy = null; } this._removeTooltip(); this._parsedModel = null; this._tensorInfoMap.clear(); } // ─── Private ─────────────────────────────────────────────────────────────── /** * Build a lookup map from tensor name → { name, shape, dataType }. * Sources: parsedModel.inputs, parsedModel.outputs, parsedModel.graph.valueInfo * @param {Object} parsedModel * @private */ _buildTensorInfoMap(parsedModel) { this._tensorInfoMap.clear(); // 1. Inputs if (Array.isArray(parsedModel.inputs)) { for (const inp of parsedModel.inputs) { if (inp.name) { this._tensorInfoMap.set(inp.name, { name: inp.name, shape: inp.shape || [], dataType: inp.dataType || 'UNKNOWN' }); } } } // 2. Outputs if (Array.isArray(parsedModel.outputs)) { for (const out of parsedModel.outputs) { if (out.name) { this._tensorInfoMap.set(out.name, { name: out.name, shape: out.shape || [], dataType: out.dataType || 'UNKNOWN' }); } } } // 3. value_info (intermediate tensors) const valueInfo = parsedModel.graph && parsedModel.graph.valueInfo; if (valueInfo && typeof valueInfo === 'object') { const entries = valueInfo instanceof Map ? valueInfo.entries() : Object.entries(valueInfo); for (const [key, vi] of entries) { if (key && !this._tensorInfoMap.has(key)) { this._tensorInfoMap.set(key, { name: vi.name || key, shape: vi.shape || [], dataType: vi.dataType || 'UNKNOWN' }); } } } } /** * Bind mouseover, mouseout, and tap events on edges. * @param {Object} cy * @private */ _bindCytoscapeEvents(cy) { this._onEdgeMouseOver = (evt) => { const edge = evt.target; const mouseEvent = evt.originalEvent; this._showEdgeTooltip(edge, mouseEvent); }; this._onEdgeMouseOut = () => { this._hideTooltip(); }; this._onEdgeTap = (evt) => { const edge = evt.target; const mouseEvent = evt.originalEvent || evt.position; this._showEdgeTooltip(edge, mouseEvent); }; cy.on('mouseover', 'edge', this._onEdgeMouseOver); cy.on('mouseout', 'edge', this._onEdgeMouseOut); cy.on('tap', 'edge', this._onEdgeTap); } /** * Unbind edge events from Cytoscape. * @param {Object} cy * @private */ _unbindCytoscapeEvents(cy) { if (this._onEdgeMouseOver) { cy.off('mouseover', 'edge', this._onEdgeMouseOver); } if (this._onEdgeMouseOut) { cy.off('mouseout', 'edge', this._onEdgeMouseOut); } if (this._onEdgeTap) { cy.off('tap', 'edge', this._onEdgeTap); } this._onEdgeMouseOver = null; this._onEdgeMouseOut = null; this._onEdgeTap = null; } /** * Show tooltip for an edge. * @param {Object} edge - Cytoscape edge element * @param {MouseEvent|Object} mouseEvent * @private */ _showEdgeTooltip(edge, mouseEvent) { if (!this._tooltip || !edge || edge.length === 0) return; const edgeData = edge.data(); const tensorName = edgeData.label || ''; const tensorInfo = this.lookupTensor(tensorName); const html = this._buildTooltipHTML(tensorName, tensorInfo); this._tooltip.innerHTML = html; this._tooltip.style.display = 'block'; this._positionTooltip(mouseEvent); } /** * Build tooltip HTML for a tensor. * @param {string} tensorName * @param {Object|null} tensorInfo * @returns {string} * @private */ _buildTooltipHTML(tensorName, tensorInfo) { const lines = []; // Tensor name const displayName = tensorName || 'Unnamed tensor'; lines.push('' + this._escapeHtml(displayName) + ''); if (tensorInfo) { // Shape const shapeStr = tensorInfo.shape && tensorInfo.shape.length > 0 ? '[' + tensorInfo.shape.join(', ') + ']' : 'unknown'; lines.push('Shape: ' + this._escapeHtml(shapeStr) + ''); // Data type lines.push('Type: ' + this._escapeHtml(tensorInfo.dataType) + ''); } else { lines.push('Shape: unknown'); } return lines.join('
'); } /** * Hide the tooltip. * @private */ _hideTooltip() { if (this._tooltip) { this._tooltip.style.display = 'none'; } } /** * Create the tooltip DOM element. * @private */ _createTooltip() { const tooltip = document.createElement('div'); tooltip.className = 'tensor-shape-tooltip'; tooltip.setAttribute('role', 'tooltip'); tooltip.style.cssText = [ 'position: fixed', 'z-index: 10000', 'background: rgba(30,30,30,0.95)', 'color: #fff', 'padding: 8px 12px', 'border-radius: 6px', 'font-size: 12px', 'max-width: 300px', 'pointer-events: none', 'display: none', 'box-shadow: 0 2px 8px rgba(0,0,0,0.4)', 'line-height: 1.5', 'border-left: 3px solid #17a2b8' ].join(';'); document.body.appendChild(tooltip); this._tooltip = tooltip; } /** * Remove tooltip from DOM. * @private */ _removeTooltip() { if (this._tooltip && this._tooltip.parentNode) { this._tooltip.parentNode.removeChild(this._tooltip); } this._tooltip = null; } /** * Position tooltip near the mouse cursor. * @param {MouseEvent|Object} mouseEvent * @private */ _positionTooltip(mouseEvent) { if (!this._tooltip || !mouseEvent) return; const offset = 12; let x, y; if (mouseEvent.clientX !== undefined) { x = mouseEvent.clientX + offset; y = mouseEvent.clientY + offset; } else if (mouseEvent.x !== undefined) { // Cytoscape position object fallback x = mouseEvent.x + offset; y = mouseEvent.y + offset; } else { return; } // Keep within viewport const rect = this._tooltip.getBoundingClientRect(); if (x + rect.width > window.innerWidth) { x = x - rect.width - offset * 2; } if (y + rect.height > window.innerHeight) { y = y - rect.height - offset * 2; } this._tooltip.style.left = x + 'px'; this._tooltip.style.top = y + 'px'; } /** * Escape HTML special characters. * @param {string} str * @returns {string} * @private */ _escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } } // Export as global for browser usage window.TensorShapeInspector = TensorShapeInspector;