model-explorer / js /ui /tensorShapeInspector.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* 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<string, Object>} 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('<strong>' + this._escapeHtml(displayName) + '</strong>');
if (tensorInfo) {
// Shape
const shapeStr = tensorInfo.shape && tensorInfo.shape.length > 0
? '[' + tensorInfo.shape.join(', ') + ']'
: 'unknown';
lines.push('<span>Shape: ' + this._escapeHtml(shapeStr) + '</span>');
// Data type
lines.push('<span>Type: ' + this._escapeHtml(tensorInfo.dataType) + '</span>');
} else {
lines.push('<span>Shape: unknown</span>');
}
return lines.join('<br>');
}
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
}
// Export as global for browser usage
window.TensorShapeInspector = TensorShapeInspector;