Spaces:
Running
Running
| /** | |
| * 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 = '<p class="text-muted">No statistics available.</p>'; | |
| return; | |
| } | |
| const { opTypeCounts, totalNodes, totalEdges, totalInitializers } = stats; | |
| // Summary section | |
| let html = ` | |
| <div class="mb-3"> | |
| <div class="d-flex flex-wrap gap-3 small"> | |
| <span><i class="fas fa-project-diagram me-1"></i><strong>${totalNodes}</strong> Nodes</span> | |
| <span><i class="fas fa-arrows-alt-h me-1"></i><strong>${totalEdges}</strong> Edges</span> | |
| <span><i class="fas fa-database me-1"></i><strong>${totalInitializers}</strong> Initializers</span> | |
| </div> | |
| </div>`; | |
| if (opTypeCounts.length === 0) { | |
| html += '<p class="text-muted">No operators found.</p>'; | |
| this._container.innerHTML = html; | |
| return; | |
| } | |
| // Operator stats table | |
| html += ` | |
| <table class="table table-sm table-hover mb-0" role="grid" aria-label="Operator statistics"> | |
| <thead> | |
| <tr> | |
| <th scope="col">Operator</th> | |
| <th scope="col" class="text-end">Count</th> | |
| <th scope="col" class="text-end">%</th> | |
| </tr> | |
| </thead> | |
| <tbody>`; | |
| opTypeCounts.forEach((entry) => { | |
| html += ` | |
| <tr class="layer-stats-row cursor-pointer" | |
| role="button" | |
| tabindex="0" | |
| data-op-type="${this._escapeHtml(entry.opType)}" | |
| title="Click to highlight all ${this._escapeHtml(entry.opType)} nodes" | |
| aria-label="${this._escapeHtml(entry.opType)}: ${entry.count} nodes, ${entry.percentage}%"> | |
| <td>${this._escapeHtml(entry.opType)}</td> | |
| <td class="text-end">${entry.count}</td> | |
| <td class="text-end">${entry.percentage}%</td> | |
| </tr>`; | |
| }); | |
| html += ` | |
| </tbody> | |
| </table>`; | |
| this._container.innerHTML = html; | |
| this._attachRowHandlers(); | |
| } | |
| /** | |
| * Clear the display. | |
| */ | |
| clear() { | |
| this._stats = null; | |
| if (!this._container) return; | |
| this._container.innerHTML = '<p class="text-muted">Select a model to view layer statistics</p>'; | |
| } | |
| /** | |
| * Get the last computed stats. | |
| * @returns {LayerStatsData|null} | |
| */ | |
| getStats() { | |
| return this._stats; | |
| } | |
| } | |
| window.LayerStats = LayerStats; | |