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