UNPKG

chartjs-chart-treenode-test

Version:
844 lines (697 loc) 28.1 kB
// chartjs-chart-treenode/src/plugin.js const treeGraphPlugin = { id: 'treeGraph', // Función para dividir texto inteligentemente wrapText(ctx, text, maxWidth) { if (!text || text.length === 0) return ['']; // Verificar si el texto completo cabe en una línea const fullTextWidth = ctx.measureText(text).width; if (fullTextWidth <= maxWidth) { return [text]; } const words = text.split(' '); if (words.length === 1) { // Si es una sola palabra muy larga, devolverla como está return [text]; } const lines = []; let currentLine = ''; for (let i = 0; i < words.length; i++) { const word = words[i]; const testLine = currentLine + (currentLine ? ' ' : '') + word; const testWidth = ctx.measureText(testLine).width; if (testWidth <= maxWidth) { currentLine = testLine; } else { if (currentLine) { lines.push(currentLine); currentLine = word; } else { currentLine = word; } } } if (currentLine) { lines.push(currentLine); } return lines.length > 0 ? lines : [text]; }, // Calcular ancho máximo basado en el espacio entre columnas/filas calculateMaxLabelWidth(chart) { if (!chart._treeGraphData) return 100; const area = chart.chartArea; const paddingX = 50; const paddingY = 40; const dataset = chart.data.datasets[0]; const isVertical = dataset.vertical === true; if (isVertical) { // En vertical, usar el ancho disponible dividido por el número de columnas const height = area.bottom - area.top - 2 * paddingY; const maxDepth = chart._treeGraphData.maxDepth || 1; const rowSpacing = height / Math.max(1, maxDepth); return Math.max(60, rowSpacing * 0.7); } else { // Lógica original para horizontal const width = area.right - area.left - 2 * paddingX; const maxDepth = chart._treeGraphData.maxDepth || 1; const columnSpacing = width / Math.max(1, maxDepth); return Math.max(60, columnSpacing * 0.7); } }, // Función para configurar las coordenadas de los nodos setupNodeCoordinates(chart) { if (chart.config.type !== 'scatter') return; const dataset = chart.data.datasets[0]; const links = dataset.data; if (!links || !Array.isArray(links)) return; function buildGraph(links) { const nodes = {}; links.forEach(link => { if (!nodes[link.from]) nodes[link.from] = { label: link.from, children: [], parents: [] }; if (!nodes[link.to]) nodes[link.to] = { label: link.to, children: [], parents: [] }; nodes[link.from].children.push(nodes[link.to]); nodes[link.to].parents.push(nodes[link.from]); }); return nodes; } // Reemplazar la función layoutGraphic existente con esta versión mejorada function layoutGraphic(nodes, dataset = {}) { // 1. Primero establecer las profundidades (depth) como antes Object.values(nodes).forEach(n => { n.depth = null; n.y = null; n.subtreeSize = 0; n.children = n.children || []; n.parents = n.parents || []; }); const roots = Object.values(nodes).filter(n => n.parents.length === 0); roots.forEach(root => root.depth = 0); let changed; do { changed = false; Object.values(nodes).forEach(node => { if (node.parents.length === 0) return; const maxParentDepth = Math.max(-1, ...node.parents.map(p => p.depth ?? -1)); const newDepth = maxParentDepth + 1; if (node.depth === null || node.depth < newDepth) { node.depth = newDepth; changed = true; } }); } while (changed); // Aplicar configuración manual de columnas si existe const columnMap = dataset.column || {}; Object.entries(nodes).forEach(([label, node]) => { if (columnMap[label] !== undefined) { node.depth = columnMap[label]; } }); // 2. Calcular el tamaño del subárbol para cada nodo (bottom-up) calculateSubtreeSizes(nodes); // 3. Asignar posiciones Y basadas en las ramificaciones (top-down) assignHierarchicalPositions(nodes, dataset); // 4. Aplicar ajustes finales y normalización normalizePositions(nodes); const maxDepth = Math.max(...Object.values(nodes).map(n => n.depth)); return { maxDepth, maxY: 1 }; } // Calcular el tamaño del subárbol de cada nodo (cuántos descendientes tiene) function calculateSubtreeSizes(nodes) { const visited = new Set(); function calculateSize(node) { if (visited.has(node.label)) return node.subtreeSize; visited.add(node.label); if (node.children.length === 0) { node.subtreeSize = 1; return 1; } let totalSize = 1; // El nodo mismo node.children.forEach(child => { totalSize += calculateSize(child); }); node.subtreeSize = totalSize; return totalSize; } Object.values(nodes).forEach(node => { if (!visited.has(node.label)) { calculateSize(node); } }); } // Asignar posiciones Y basadas en la estructura jerárquica function assignHierarchicalPositions(nodes, dataset) { const priorityMap = dataset.priority || {}; // Organizar nodos por profundidad const nodesByDepth = new Map(); Object.values(nodes).forEach(node => { if (!nodesByDepth.has(node.depth)) { nodesByDepth.set(node.depth, []); } nodesByDepth.get(node.depth).push(node); }); // Procesar cada nivel de profundidad for (let depth = 0; depth <= Math.max(...nodesByDepth.keys()); depth++) { const nodesAtDepth = nodesByDepth.get(depth) || []; if (depth === 0) { // Nodos raíz: distribuir uniformemente assignRootPositions(nodesAtDepth, priorityMap); } else { // Nodos no raíz: posicionar basado en sus padres assignChildPositions(nodesAtDepth, priorityMap); } } } // Asignar posiciones a los nodos raíz function assignRootPositions(rootNodes, priorityMap) { // Ordenar por prioridad y por tamaño de subárbol rootNodes.sort((a, b) => { const aPriority = priorityMap[a.label] ?? Number.MAX_SAFE_INTEGER; const bPriority = priorityMap[b.label] ?? Number.MAX_SAFE_INTEGER; if (aPriority !== bPriority) return aPriority - bPriority; // Si tienen la misma prioridad, ordenar por tamaño de subárbol (más grande primero) if (a.subtreeSize !== b.subtreeSize) return b.subtreeSize - a.subtreeSize; return a.label.localeCompare(b.label); }); // Asignar posiciones espaciadas uniformemente const spacing = 1 / (rootNodes.length + 1); rootNodes.forEach((node, index) => { node.y = spacing * (index + 1); node.assignedSpace = calculateNodeSpace(node, rootNodes.length); }); } // Asignar posiciones a nodos hijos basándose en sus padres function assignChildPositions(childNodes, priorityMap) { // Agrupar nodos por sus padres const nodeGroups = groupNodesByParents(childNodes); nodeGroups.forEach(group => { assignGroupPositions(group, priorityMap); }); } // Agrupar nodos por sus combinaciones de padres function groupNodesByParents(nodes) { const groups = new Map(); nodes.forEach(node => { // Crear una clave única basada en los padres const parentKey = node.parents .map(p => p.label) .sort() .join('|'); if (!groups.has(parentKey)) { groups.set(parentKey, { parents: node.parents, children: [] }); } groups.get(parentKey).children.push(node); }); return Array.from(groups.values()); } // Asignar posiciones a un grupo de nodos que comparten padres function assignGroupPositions(group, priorityMap) { const { parents, children } = group; // Ordenar hijos por prioridad y tamaño de subárbol children.sort((a, b) => { const aPriority = priorityMap[a.label] ?? Number.MAX_SAFE_INTEGER; const bPriority = priorityMap[b.label] ?? Number.MAX_SAFE_INTEGER; if (aPriority !== bPriority) return aPriority - bPriority; if (a.subtreeSize !== b.subtreeSize) return b.subtreeSize - a.subtreeSize; return a.label.localeCompare(b.label); }); // Calcular la posición central basada en los padres const centerY = calculateParentCenter(parents); // Calcular el espacio disponible para este grupo const availableSpace = calculateAvailableSpace(parents, children); // Distribuir los hijos alrededor del centro distributeChildrenAroundCenter(children, centerY, availableSpace); } // Calcular la posición Y central de los padres function calculateParentCenter(parents) { if (parents.length === 0) return 0.5; const parentPositions = parents.map(p => p.y).filter(y => y !== null); if (parentPositions.length === 0) return 0.5; // Usar el promedio ponderado por el tamaño del subárbol let totalWeight = 0; let weightedSum = 0; parents.forEach(parent => { if (parent.y !== null) { const weight = parent.subtreeSize || 1; weightedSum += parent.y * weight; totalWeight += weight; } }); return totalWeight > 0 ? weightedSum / totalWeight : 0.5; } // Calcular el espacio disponible para un grupo de hijos function calculateAvailableSpace(parents, children) { // Calcular el espacio basado en el tamaño total de los subárboles de los padres const totalParentSubtreeSize = parents.reduce((sum, parent) => sum + (parent.subtreeSize || 1), 0); const totalChildrenSize = children.reduce((sum, child) => sum + (child.subtreeSize || 1), 0); // El espacio disponible es proporcional al tamaño del subárbol const baseSpace = Math.min(0.8, totalChildrenSize / totalParentSubtreeSize * 0.6); return Math.max(0.1, baseSpace); // Mínimo 10% del espacio total } // Distribuir hijos alrededor del centro function distributeChildrenAroundCenter(children, centerY, availableSpace) { if (children.length === 0) return; if (children.length === 1) { children[0].y = centerY; children[0].assignedSpace = availableSpace; return; } // Calcular posiciones basadas en el tamaño de los subárboles const totalSubtreeSize = children.reduce((sum, child) => sum + (child.subtreeSize || 1), 0); let currentOffset = -availableSpace / 2; children.forEach(child => { const childProportion = (child.subtreeSize || 1) / totalSubtreeSize; const childSpace = availableSpace * childProportion; child.y = centerY + currentOffset + childSpace / 2; child.assignedSpace = childSpace; currentOffset += childSpace; }); } // Calcular el espacio que debe ocupar un nodo function calculateNodeSpace(node, totalNodesAtLevel) { const baseSpace = 1 / totalNodesAtLevel; const subtreeFactor = Math.log(node.subtreeSize || 1) + 1; return baseSpace * subtreeFactor; } // Normalizar todas las posiciones Y para que estén entre 0 y 1 function normalizePositions(nodes) { const allNodes = Object.values(nodes); const validYs = allNodes.map(n => n.y).filter(y => y !== null); if (validYs.length === 0) return; const minY = Math.min(...validYs); const maxY = Math.max(...validYs); const range = maxY - minY; if (range === 0) { // Todos los nodos tienen la misma Y, centrarlos allNodes.forEach(node => { if (node.y !== null) node.y = 0.5; }); } else { // Normalizar al rango [0.1, 0.9] para dejar márgenes allNodes.forEach(node => { if (node.y !== null) { node.y = 0.1 + 0.8 * ((node.y - minY) / range); } }); } // Ajustar posiciones para evitar solapamientos resolveOverlaps(nodes); } // Resolver solapamientos entre nodos en la misma profundidad function resolveOverlaps(nodes) { const nodesByDepth = new Map(); Object.values(nodes).forEach(node => { if (!nodesByDepth.has(node.depth)) { nodesByDepth.set(node.depth, []); } nodesByDepth.get(node.depth).push(node); }); nodesByDepth.forEach(nodesAtDepth => { if (nodesAtDepth.length <= 1) return; // Ordenar por posición Y nodesAtDepth.sort((a, b) => a.y - b.y); const minSeparation = 0.08; // Separación mínima entre nodos // Ajustar posiciones para evitar solapamientos for (let i = 1; i < nodesAtDepth.length; i++) { const prevNode = nodesAtDepth[i - 1]; const currentNode = nodesAtDepth[i]; const minY = prevNode.y + minSeparation; if (currentNode.y < minY) { currentNode.y = minY; } } // Renormalizar si es necesario const lastNode = nodesAtDepth[nodesAtDepth.length - 1]; if (lastNode.y > 0.9) { const excess = lastNode.y - 0.9; nodesAtDepth.forEach(node => { node.y -= excess; }); } }); } const nodes = buildGraph(links); const { maxDepth } = layoutGraphic(nodes); const allNodes = Object.values(nodes); // Calcular valores de los nodos allNodes.forEach(node => { const incoming = links.filter(l => l.to === node.label); const outgoing = links.filter(l => l.from === node.label); if (incoming.length > 0) { node.value = incoming.reduce((acc, l) => acc + l.value, 0); } else if (outgoing.length > 0) { node.value = outgoing[0].value; } else { node.value = 0; } }); // Función para convertir a coordenadas del canvas function toCanvasCoords(depth, y) { const area = chart.chartArea; const paddingX = 50, paddingY = 40; const width = area.right - area.left - 2 * paddingX; const height = area.bottom - area.top - 2 * paddingY; const minY = Math.min(...allNodes.map(n => n.y)); const maxY = Math.max(...allNodes.map(n => n.y)); const yNorm = (y - minY) / (maxY - minY || 1); const isVertical = dataset.vertical === true; if (isVertical) { // En orientación vertical: intercambiar X e Y return { x: area.left + paddingX + yNorm * width, y: area.top + paddingY + (depth / (maxDepth || 1)) * height }; } else { // Orientación horizontal original return { x: area.left + paddingX + (depth / (maxDepth || 1)) * width, y: area.top + paddingY + yNorm * height }; } } // Guardar datos en el chart para uso posterior chart._treeGraphData = { nodes, allNodes, maxDepth, links, toCanvasCoords }; return chart._treeGraphData; }, // Configurar el event listener del mouse setupMouseListener(chart) { if (chart._tooltipHandlerAttached) return; const canvas = chart.canvas; chart._hoveredNode = null; canvas.addEventListener("mousemove", (e) => { if (!chart._treeGraphData) return; const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; chart._hoveredNode = null; for (const node of chart._treeGraphData.allNodes) { const { x, y } = chart._treeGraphData.toCanvasCoords(node.depth, node.y); const dx = mouseX - x; const dy = mouseY - y; if (Math.sqrt(dx * dx + dy * dy) <= 10) { chart._hoveredNode = node; break; } } chart.draw(); }); chart._tooltipHandlerAttached = true; }, // Calcular posición inteligente del tooltip calculateTooltipPosition(chart, nodeX, nodeY, tooltipWidth, tooltipHeight) { const area = chart.chartArea; const padding = 10; // Separación del nodo const margin = 8; // Margen del borde del canvas // Posición por defecto: arriba del nodo let x = nodeX - tooltipWidth / 2; let y = nodeY - tooltipHeight - padding; let position = 'top'; // Verificar si se sale por arriba if (y < area.top + margin) { // Mover abajo del nodo y = nodeY + padding; position = 'bottom'; } // Verificar si se sale por la izquierda if (x < area.left + margin) { if (position === 'top' || position === 'bottom') { // Mover a la derecha del nodo x = nodeX + padding; y = nodeY - tooltipHeight / 2; position = 'right'; } else { x = area.left + margin; } } // Verificar si se sale por la derecha if (x + tooltipWidth > area.right - margin) { if (position === 'top' || position === 'bottom') { // Mover a la izquierda del nodo x = nodeX - tooltipWidth - padding; y = nodeY - tooltipHeight / 2; position = 'left'; } else { x = area.right - tooltipWidth - margin; } } // Verificar si se sale por abajo (cuando está en posición bottom) if (y + tooltipHeight > area.bottom - margin) { if (position === 'bottom') { // Volver arriba del nodo si hay espacio const topY = nodeY - tooltipHeight - padding; if (topY >= area.top + margin) { y = topY; position = 'top'; } else { y = area.bottom - tooltipHeight - margin; } } else if (position === 'left' || position === 'right') { y = Math.min(y, area.bottom - tooltipHeight - margin); } } // Verificar límites verticales para posiciones left/right if (position === 'left' || position === 'right') { if (y < area.top + margin) { y = area.top + margin; } else if (y + tooltipHeight > area.bottom - margin) { y = area.bottom - tooltipHeight - margin; } } return { x, y, position }; }, // Dibujar tooltip con estilo ChartJS drawTooltip(chart, node) { const ctx = chart.ctx; const { x: nodeX, y: nodeY } = chart._treeGraphData.toCanvasCoords(node.depth, node.y); // Configuración del tooltip const fontSize = 13; const fontFamily = 'Helvetica, Arial, sans-serif'; const padding = 8; const borderRadius = 6; const shadowBlur = 4; const shadowOffset = 2; // Preparar texto const title = node.label; const datasetLabel = chart.data.datasets[0].label || 'Value'; const value = `${datasetLabel}: ${node.value}`; const lines = [title, value]; ctx.save(); ctx.font = `${fontSize}px ${fontFamily}`; // Calcular dimensiones del tooltip const lineHeight = fontSize + 4; const maxWidth = Math.max(...lines.map(line => ctx.measureText(line).width)); const tooltipWidth = maxWidth + (padding * 2); const tooltipHeight = (lines.length * lineHeight) + (padding * 2) - 4; // -4 para ajustar espaciado // Calcular posición inteligente const { x: tooltipX, y: tooltipY, position } = this.calculateTooltipPosition( chart, nodeX, nodeY, tooltipWidth, tooltipHeight ); // Dibujar sombra ctx.shadowColor = 'rgba(0, 0, 0, 0.2)'; ctx.shadowBlur = shadowBlur; ctx.shadowOffsetX = shadowOffset; ctx.shadowOffsetY = shadowOffset; // Dibujar fondo del tooltip ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; this.drawRoundedRect(ctx, tooltipX, tooltipY, tooltipWidth, tooltipHeight, borderRadius); ctx.fill(); // Resetear sombra ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; // Dibujar borde ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; ctx.lineWidth = 1; this.drawRoundedRect(ctx, tooltipX, tooltipY, tooltipWidth, tooltipHeight, borderRadius); ctx.stroke(); // Dibujar flecha indicadora (opcional, basada en la posición) this.drawTooltipArrow(ctx, nodeX, nodeY, tooltipX, tooltipY, tooltipWidth, tooltipHeight, position); // Dibujar texto ctx.fillStyle = '#ffffff'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; lines.forEach((line, index) => { const textY = tooltipY + padding + (index * lineHeight); const textX = tooltipX + padding; if (index === 0) { // Título en negrita ctx.font = `bold ${fontSize}px ${fontFamily}`; ctx.fillStyle = '#ffffff'; } else { // Contenido regular ctx.font = `${fontSize}px ${fontFamily}`; ctx.fillStyle = '#cccccc'; } ctx.fillText(line, textX, textY); }); ctx.restore(); }, // Dibujar rectángulo con esquinas redondeadas drawRoundedRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); }, // Dibujar flecha del tooltip drawTooltipArrow(ctx, nodeX, nodeY, tooltipX, tooltipY, tooltipWidth, tooltipHeight, position) { const arrowSize = 6; ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; ctx.beginPath(); switch (position) { case 'top': // Flecha apuntando hacia abajo const centerX = tooltipX + tooltipWidth / 2; ctx.moveTo(centerX - arrowSize, tooltipY + tooltipHeight); ctx.lineTo(centerX + arrowSize, tooltipY + tooltipHeight); ctx.lineTo(centerX, tooltipY + tooltipHeight + arrowSize); break; case 'bottom': // Flecha apuntando hacia arriba const centerXBottom = tooltipX + tooltipWidth / 2; ctx.moveTo(centerXBottom - arrowSize, tooltipY); ctx.lineTo(centerXBottom + arrowSize, tooltipY); ctx.lineTo(centerXBottom, tooltipY - arrowSize); break; case 'left': // Flecha apuntando hacia la derecha const centerYLeft = tooltipY + tooltipHeight / 2; ctx.moveTo(tooltipX + tooltipWidth, centerYLeft - arrowSize); ctx.lineTo(tooltipX + tooltipWidth, centerYLeft + arrowSize); ctx.lineTo(tooltipX + tooltipWidth + arrowSize, centerYLeft); break; case 'right': // Flecha apuntando hacia la izquierda const centerYRight = tooltipY + tooltipHeight / 2; ctx.moveTo(tooltipX, centerYRight - arrowSize); ctx.lineTo(tooltipX, centerYRight + arrowSize); ctx.lineTo(tooltipX - arrowSize, centerYRight); break; } ctx.closePath(); ctx.fill(); }, afterDraw(chart) { const data = this.setupNodeCoordinates(chart); if (!data) return; this.setupMouseListener(chart); this.drawGraph(chart, data); }, afterResize(chart) { // Recalcular coordenadas después del resize this.setupNodeCoordinates(chart); }, drawGraph(chart, { allNodes, links, toCanvasCoords }) { const ctx = chart.ctx; const dataset = chart.data.datasets[0]; const isVertical = dataset.vertical === true; // Dibujar conexiones allNodes.forEach(node => { node.children.forEach(child => { const from = toCanvasCoords(node.depth, node.y); const to = toCanvasCoords(child.depth, child.y); ctx.save(); ctx.strokeStyle = "#888"; ctx.lineWidth = 2; ctx.beginPath(); if (isVertical) { // En orientación vertical, las curvas van de arriba hacia abajo const dy = (to.y - from.y) * 0.5; ctx.moveTo(from.x, from.y); ctx.bezierCurveTo(from.x, from.y + dy, to.x, to.y - dy, to.x, to.y); } else { // Orientación horizontal original const dx = (to.x - from.x) * 0.5; ctx.moveTo(from.x, from.y); ctx.bezierCurveTo(from.x + dx, from.y, to.x - dx, to.y, to.x, to.y); } ctx.stroke(); ctx.restore(); }); }); // Dibujar nodos allNodes.forEach(node => { const { x, y } = toCanvasCoords(node.depth, node.y); ctx.save(); const nodeColors = dataset.nodeColors || {}; const fillColor = nodeColors[node.label] || "black"; const nodeRadius = dataset.nodeRadius || 10; const borderRaw = dataset.nodeBorder || "0px"; const borderColor = dataset.nodeBorderColor || "black"; let borderWidth = 0; if (typeof borderRaw === "string" && borderRaw.endsWith("px")) { borderWidth = parseInt(borderRaw.replace("px", "")); } if (borderWidth > 0) { ctx.beginPath(); ctx.arc(x, y, nodeRadius + borderWidth / 2, 0, 2 * Math.PI); ctx.fillStyle = borderColor; ctx.fill(); } ctx.beginPath(); ctx.arc(x, y, nodeRadius, 0, 2 * Math.PI); ctx.fillStyle = fillColor; ctx.fill(); const fontSize = dataset.labelFontSize || 12; const fontWeight = dataset.labelFontWeight || "bold"; const fontFamily = dataset.labelFontFamily || "sans-serif"; ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`; ctx.fillStyle = "#222"; ctx.textAlign = "center"; ctx.textBaseline = "top"; // Calcular ancho máximo y dividir el texto const maxLabelWidth = this.calculateMaxLabelWidth(chart); const showValue = dataset.showValue ?? false; const textLines = this.wrapText(ctx, node.label, maxLabelWidth); if (showValue) { textLines.push(`(${node.value})`); } // Dibujar cada línea de texto const lineHeight = fontSize + 2; let startY, startX; if (isVertical) { // En orientación vertical, posicionar el texto a la derecha del nodo startX = x + nodeRadius + 8; startY = y - (textLines.length * lineHeight) / 2; ctx.textAlign = "left"; ctx.textBaseline = "top"; } else { // Orientación horizontal original - texto debajo del nodo startX = x; startY = y + nodeRadius + 4; ctx.textAlign = "center"; ctx.textBaseline = "top"; } textLines.forEach((line, index) => { const lineY = startY + (index * lineHeight); ctx.fillText(line, startX, lineY); }); ctx.restore(); }); // Dibujar tooltip mejorado si hay un nodo hovereado const hovered = chart._hoveredNode; if (hovered) { this.drawTooltip(chart, hovered); } } }; if (typeof window !== 'undefined' && window.Chart) { window.Chart.register(treeGraphPlugin); } export default treeGraphPlugin;