220 lines
6.2 KiB
JavaScript
220 lines
6.2 KiB
JavaScript
|
import React, {useState, useRef, useEffect} from 'react';
|
|||
|
import Springy from 'springy';
|
|||
|
|
|||
|
/**
|
|||
|
* Crée un identifiant unique pour l’arête identifiée par ses deux extrémités.
|
|||
|
*
|
|||
|
* @param from Premier nœud de l’arête.
|
|||
|
* @param to Second nœud de l’arête.
|
|||
|
* @return Identifiant unique pour les deux directions de l’arête.
|
|||
|
*/
|
|||
|
const getEdgeId = (from, to) =>
|
|||
|
{
|
|||
|
if (to < from)
|
|||
|
{
|
|||
|
return getEdgeId(to, from);
|
|||
|
}
|
|||
|
|
|||
|
return `${from}-${to}`;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Vérifie si une liste d’arêtes contient une arête donnée, dans un sens ou
|
|||
|
* dans l’autre.
|
|||
|
*
|
|||
|
* @param list Liste d’arêtes.
|
|||
|
* @param from Premier nœud de l’arête.
|
|||
|
* @param to Second nœud de l’arête.
|
|||
|
* @return Vrai si et seulement si l’arête existe.
|
|||
|
*/
|
|||
|
const includesEdge = (list, from, to) =>
|
|||
|
list.some(([source, target]) => (
|
|||
|
(source === from && target === to)
|
|||
|
|| (source === to && target === from)
|
|||
|
));
|
|||
|
|
|||
|
/**
|
|||
|
* Affiche un graphe.
|
|||
|
*
|
|||
|
* @prop nodes Liste des identifiants de nœuds du graphe. Chaque nœud doit
|
|||
|
* avoir un identifiant unique ne contenant pas de tiret ('-').
|
|||
|
* @prop edges Couples d’identifiants de nœuds formant les arêtes du graphe.
|
|||
|
* @prop render Fonction de rendu prenant en paramètre l’identifiant d’un nœud
|
|||
|
* du graphe et renvoyant un élément à afficher pour le représenter.
|
|||
|
*/
|
|||
|
const Graph = ({nodes, edges, render}) =>
|
|||
|
{
|
|||
|
const [graph,] = useState(new Springy.Graph());
|
|||
|
const [layout,] = useState(new Springy.Layout.ForceDirected(
|
|||
|
graph,
|
|||
|
/* rigidité = */ 400,
|
|||
|
/* répulsion = */ 400,
|
|||
|
/* amortissement = */ 0.5
|
|||
|
));
|
|||
|
|
|||
|
// N’arrête jamais l’animation
|
|||
|
layout.minEnergyThreshold = 0;
|
|||
|
|
|||
|
const graphParent = useRef(null);
|
|||
|
|
|||
|
// Ajout des nouveaux nœuds
|
|||
|
for (let node of nodes)
|
|||
|
{
|
|||
|
if (!(node in graph.nodeSet))
|
|||
|
{
|
|||
|
console.info(`Ajout du nouveau nœud ${node}`);
|
|||
|
graph.addNode(new Springy.Node(node));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Retrait des anciens nœuds et de leurs arêtes adjacentes
|
|||
|
graph.filterNodes(node =>
|
|||
|
{
|
|||
|
if (!nodes.includes(node.id))
|
|||
|
{
|
|||
|
console.info(`Retrait de l’ancien nœud ${node.id}`);
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
});
|
|||
|
|
|||
|
// Ajout des nouvelles arêtes
|
|||
|
for (let [from, to] of edges)
|
|||
|
{
|
|||
|
const edgeId = getEdgeId(from, to);
|
|||
|
|
|||
|
if (graph.edges.every(edge => edge.id !== edgeId))
|
|||
|
{
|
|||
|
console.info(`Ajout de la nouvelle arête ${edgeId}`);
|
|||
|
graph.addEdge(new Springy.Edge(edgeId, {id: from}, {id: to}));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Retrait des anciennes arêtes
|
|||
|
graph.filterEdges(({source: {id: from}, target: {id: to}}) =>
|
|||
|
{
|
|||
|
if (!includesEdge(edges, from, to))
|
|||
|
{
|
|||
|
console.info(`Retrait de l’ancienne arête ${from}-${to}`);
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
});
|
|||
|
|
|||
|
useEffect(() =>
|
|||
|
{
|
|||
|
const center = new Springy.Vector(
|
|||
|
window.innerWidth / 2,
|
|||
|
window.innerHeight / 2
|
|||
|
);
|
|||
|
|
|||
|
const scale = 50;
|
|||
|
const coordsToScreen = vec => vec.multiply(scale).add(center);
|
|||
|
const screenToCoords = vec => vec.subtract(center).divide(scale);
|
|||
|
|
|||
|
layout.start(() =>
|
|||
|
{
|
|||
|
layout.eachNode(({id}, {p}) =>
|
|||
|
{
|
|||
|
const element = graphParent.current.querySelector(
|
|||
|
`[data-node-id="${id.replace('"', '\\"')}"]`
|
|||
|
);
|
|||
|
|
|||
|
const {x, y} = coordsToScreen(p);
|
|||
|
|
|||
|
element.style.transform = `translate(
|
|||
|
calc(${x}px - 50%),
|
|||
|
calc(${y}px - 50%)
|
|||
|
)`;
|
|||
|
});
|
|||
|
|
|||
|
layout.eachEdge(({id}, {point1: {p: p1}, point2: {p: p2}}) =>
|
|||
|
{
|
|||
|
const element = graphParent.current.querySelector(
|
|||
|
`[data-edge-id="${id}"]`
|
|||
|
);
|
|||
|
|
|||
|
const {x: x1, y: y1} = coordsToScreen(p1);
|
|||
|
const {x: x2, y: y2} = coordsToScreen(p2);
|
|||
|
|
|||
|
element.setAttribute('x1', x1);
|
|||
|
element.setAttribute('y1', y1);
|
|||
|
element.setAttribute('x2', x2);
|
|||
|
element.setAttribute('y2', y2);
|
|||
|
});
|
|||
|
}, () => {
|
|||
|
console.info(`Fin du rendu du graphe`);
|
|||
|
}, () => {
|
|||
|
console.info(`Démarrage du rendu du graphe`);
|
|||
|
});
|
|||
|
|
|||
|
let dragging = null;
|
|||
|
|
|||
|
const mouseDown = ev =>
|
|||
|
{
|
|||
|
const {clientX: x, clientY: y} = ev;
|
|||
|
const screen = new Springy.Vector(x, y);
|
|||
|
const position = screenToCoords(screen);
|
|||
|
const nearest = layout.nearest(position);
|
|||
|
|
|||
|
if (nearest.distance <= scale / 50)
|
|||
|
{
|
|||
|
dragging = nearest;
|
|||
|
dragging.point.m = 1000;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
const mouseMove = ev =>
|
|||
|
{
|
|||
|
if (dragging !== null)
|
|||
|
{
|
|||
|
const {clientX: x, clientY: y} = ev;
|
|||
|
const screen = new Springy.Vector(x, y);
|
|||
|
dragging.point.p = screenToCoords(screen);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
const mouseUp = ev =>
|
|||
|
{
|
|||
|
if (dragging !== null)
|
|||
|
{
|
|||
|
dragging.point.m = 1;
|
|||
|
dragging = null;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
graphParent.current.addEventListener('mousedown', mouseDown);
|
|||
|
document.body.addEventListener('mousemove', mouseMove);
|
|||
|
document.body.addEventListener('mouseup', mouseUp);
|
|||
|
|
|||
|
return () =>
|
|||
|
{
|
|||
|
layout.stop();
|
|||
|
graphParent.current.removeEventListener('mousedown', mouseDown);
|
|||
|
document.body.removeEventListener('mousemove', mouseMove);
|
|||
|
document.body.removeEventListener('mouseup', mouseUp);
|
|||
|
};
|
|||
|
});
|
|||
|
|
|||
|
return (
|
|||
|
<div ref={graphParent} className="Graph">
|
|||
|
<svg className="Graph_edgesContainer">
|
|||
|
{edges.map(edge => (
|
|||
|
<line data-edge-id={getEdgeId(...edge)} />
|
|||
|
))}
|
|||
|
</svg>
|
|||
|
|
|||
|
{nodes.map(id => (
|
|||
|
<span
|
|||
|
className="Graph_node"
|
|||
|
data-node-id={id}
|
|||
|
>{render(id)}</span>
|
|||
|
))}
|
|||
|
</div>
|
|||
|
);
|
|||
|
};
|
|||
|
|
|||
|
export default Graph;
|