/** * LayerStats - Displays operator type statistics for a loaded ONNX model * Computes counts, percentages, and summary totals; highlights nodes by type on click. * Requirements: 19.1, 19.2, 19.3, 19.4, 19.5 */ class LayerStats { /** * @param {string} containerId - ID of the container element */ constructor(containerId) { this._containerId = containerId; this._container = document.getElementById(containerId); this._stats = null; if (!this._container) { console.warn(`[LayerStats] Container #${containerId} not found`); } this._setupEventListeners(); } // ─── Private ────────────────────────────────────────────────────────────── /** * Listen for model:loaded events to auto-update stats. */ _setupEventListeners() { if (window.EventBus && CONFIG && CONFIG.EVENTS) { window.EventBus.on(CONFIG.EVENTS.MODEL_LOADED, (data) => { if (data && data.model) { const stats = this.compute(data.model); this.render(stats); } }); } } /** * Escape HTML special characters. * @param {string} str * @returns {string} */ _escapeHtml(str) { const div = document.createElement('div'); div.appendChild(document.createTextNode(String(str))); return div.innerHTML; } /** * Attach click and keyboard handlers to operator rows in the stats table. */ _attachRowHandlers() { if (!this._container) return; const rows = this._container.querySelectorAll('.layer-stats-row'); rows.forEach((row) => { const handler = () => { const opType = row.dataset.opType; if (opType && window.EventBus) { window.EventBus.emit('layerstats:highlight', { opType }); } }; row.addEventListener('click', handler); row.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } }); }); } // ─── Public API ─────────────────────────────────────────────────────────── /** * Compute operator statistics from a parsed model. * @param {ParsedModel} parsedModel * @returns {LayerStatsData} */ compute(parsedModel) { const nodes = (parsedModel && parsedModel.graph && parsedModel.graph.nodes) ? parsedModel.graph.nodes : []; const edges = (parsedModel && parsedModel.graph && parsedModel.graph.edges) ? parsedModel.graph.edges : []; const initializers = (parsedModel && parsedModel.initializers) ? parsedModel.initializers : []; const totalNodes = nodes.length; const totalEdges = edges.length; const totalInitializers = initializers.length; // Count each operator type const countMap = {}; nodes.forEach((node) => { const op = node.opType || 'Unknown'; countMap[op] = (countMap[op] || 0) + 1; }); // Build sorted array with percentages const opTypeCounts = Object.entries(countMap) .map(([opType, count]) => ({ opType, count, percentage: totalNodes > 0 ? parseFloat(((count / totalNodes) * 100).toFixed(1)) : 0, })) .sort((a, b) => b.count - a.count); this._stats = { opTypeCounts, totalNodes, totalEdges, totalInitializers }; return this._stats; } /** * Render the statistics table into the container. * @param {LayerStatsData} stats */ render(stats) { if (!this._container) return; if (!stats) { this._container.innerHTML = '
No statistics available.
'; return; } const { opTypeCounts, totalNodes, totalEdges, totalInitializers } = stats; // Summary section let html = `No operators found.
'; this._container.innerHTML = html; return; } // Operator stats table html += `| Operator | Count | % |
|---|---|---|
| ${this._escapeHtml(entry.opType)} | ${entry.count} | ${entry.percentage}% |
Select a model to view layer statistics
'; } /** * Get the last computed stats. * @returns {LayerStatsData|null} */ getStats() { return this._stats; } } window.LayerStats = LayerStats;