wikimedica-disease-search/app/Graph.js

220 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {useState, useRef, useEffect} from 'react';
import Springy from 'springy';
/**
* Crée un identifiant unique pour larête identifiée par ses deux extrémités.
*
* @param from Premier nœud de larête.
* @param to Second nœud de larête.
* @return Identifiant unique pour les deux directions de larête.
*/
const getEdgeId = (from, to) =>
{
if (to < from)
{
return getEdgeId(to, from);
}
return `${from}-${to}`;
};
/**
* Vérifie si une liste darêtes contient une arête donnée, dans un sens ou
* dans lautre.
*
* @param list Liste darêtes.
* @param from Premier nœud de larête.
* @param to Second nœud de larête.
* @return Vrai si et seulement si larê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 didentifiants de nœuds formant les arêtes du graphe.
* @prop render Fonction de rendu prenant en paramètre lidentifiant dun 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
));
// Narrête jamais lanimation
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 lancien 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 lancienne 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;