app: Améliorations de performance

This commit is contained in:
Mattéo Delabre 2019-12-02 00:18:52 -05:00
parent 2efc32c5a2
commit 4295a94edf
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
2 changed files with 112 additions and 49 deletions

View File

@ -1,6 +1,14 @@
import React, {useState, useRef, useEffect} from 'react'; import React, {useState, useRef, useEffect} from 'react';
import Springy from 'springy'; import Springy from 'springy';
/**
* Échappe une valeur utilisée dans un sélecteur dattributs CSS.
*
* @param value Valeur à échapper.
* @return Valeur échappée.
*/
const escapeAttrValue = value => value.replace(/"/g, '\\"');
/** /**
* Crée un identifiant unique pour larête identifiée par ses deux extrémités. * Crée un identifiant unique pour larête identifiée par ses deux extrémités.
* *
@ -12,10 +20,10 @@ const getEdgeId = (from, to) =>
{ {
if (to < from) if (to < from)
{ {
return getEdgeId(to, from); [to, from] = [from, to];
} }
return `${from}-${to}`; return JSON.stringify([from, to]);
}; };
/** /**
@ -33,11 +41,54 @@ const includesEdge = (list, from, to) =>
|| (source === to && target === from) || (source === to && target === from)
)); ));
/**
* Recherche parmi les descendants dun élément celui qui représente un
* nœud donnée, sil existe.
*
* @param parent Élément parent.
* @param id Identifiant du nœud recherché.
* @return Élément trouvé ou null sinon.
*/
const findNode = (parent, id) =>
parent.querySelector(`[data-node-id="${escapeAttrValue(id)}"]`);
/**
* Recherche parmi les descendants dun élément celui qui représente une
* arête donnée, sil existe.
*
* @param parent Élément parent.
* @param id Identifiant de larête recherchée.
* @return Élément trouvé ou null sinon.
*/
const findEdge = (parent, id) =>
parent.querySelector(`[data-edge-id="${escapeAttrValue(id)}"]`);
/**
* Recherche le premier élément parent représentant un nœud et renvoie
* lidentifiant de ce nœud.
*
* @param start Élément de départ.
* @return Identifiant de lélément trouvé ou null sinon.
*/
const findParentNode = start =>
{
if (start instanceof window.HTMLDocument)
{
return null;
}
if (start.hasAttribute('data-node-id'))
{
return start.getAttribute('data-node-id');
}
return findParentNode(start.parentNode);
};
/** /**
* Affiche un graphe. * Affiche un graphe.
* *
* @prop nodes Liste des identifiants de nœuds du graphe. Chaque nœud doit * @prop nodes Liste des identifiants de nœuds du graphe.
* avoir un identifiant unique ne contenant pas de tiret ('-').
* @prop edges Couples didentifiants de nœuds formant les arêtes du graphe. * @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 * @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. * du graphe et renvoyant un élément à afficher pour le représenter.
@ -47,64 +98,82 @@ const Graph = ({nodes, edges, render}) =>
const [graph,] = useState(new Springy.Graph()); const [graph,] = useState(new Springy.Graph());
const [layout,] = useState(new Springy.Layout.ForceDirected( const [layout,] = useState(new Springy.Layout.ForceDirected(
graph, graph,
/* rigidité = */ 400, /* rigidité des arêtes = */ 400,
/* répulsion = */ 450, /* répulsion des nœuds = */ 450,
/* amortissement = */ 0.5 /* amortissement de vitesse = */ 0.4
)); ));
// Narrête jamais lanimation // Narrête jamais lanimation
layout.minEnergyThreshold = 0; layout.minEnergyThreshold = 0;
// Pointeur sur lélément englobant le graphe
const graphParent = useRef(null); const graphParent = useRef(null);
// Ajout des nouveaux nœuds // Ajout des nouveaux nœuds et retrait des anciens
const oldNodes = new Set(Object.keys(graph.nodeSet));
for (let node of nodes) for (let node of nodes)
{ {
if (!(node in graph.nodeSet)) if (!oldNodes.has(node))
{ {
console.info(`Ajout du nouveau nœud ${node}`);
graph.addNode(new Springy.Node(node)); graph.addNode(new Springy.Node(node));
} }
} }
// Retrait des anciens nœuds et de leurs arêtes adjacentes for (let node of oldNodes)
graph.filterNodes(node =>
{ {
if (!nodes.includes(node.id)) if (!nodes.includes(node))
{ {
console.info(`Retrait de lancien nœud ${node.id}`); graph.removeNode({id: node});
return false;
} }
}
return true; const oldNodePoints = new Set(Object.keys(layout.nodePoints));
});
for (let node of oldNodePoints)
{
if (!nodes.includes(node))
{
delete layout.nodePoints[node];
}
}
// Ajout des nouvelles arêtes et retrait des anciennes
const newEdges = new Set(edges.map(edge => getEdgeId(...edge)));
const oldEdges = new Set(graph.edges.map(edge => edge.id));
// Ajout des nouvelles arêtes
for (let [from, to] of edges) for (let [from, to] of edges)
{ {
const edgeId = getEdgeId(from, to); const edgeId = getEdgeId(from, to);
if (graph.edges.every(edge => edge.id !== edgeId)) if (!oldEdges.has(edgeId))
{ {
console.info(`Ajout de la nouvelle arête ${edgeId}`);
graph.addEdge(new Springy.Edge(edgeId, {id: from}, {id: to})); graph.addEdge(new Springy.Edge(edgeId, {id: from}, {id: to}));
} }
} }
// Retrait des anciennes arêtes for (let edge of oldEdges)
graph.filterEdges(({source: {id: from}, target: {id: to}}) =>
{ {
if (!includesEdge(edges, from, to)) if (!newEdges.has(edge))
{ {
console.info(`Retrait de lancienne arête ${from}-${to}`); graph.removeEdge({id: edge});
return false;
} }
}
return true; const oldEdgeSprings = new Set(Object.keys(layout.edgeSprings));
});
for (let edge of oldEdgeSprings)
{
if (!newEdges.has(edge))
{
delete layout.edgeSprings[edge];
}
}
// Rendu de lanimation du graphe
useEffect(() => useEffect(() =>
{ {
const center = () => new Springy.Vector( const center = () => new Springy.Vector(
window.innerWidth / 2, window.innerWidth / 2,
window.innerHeight / 2 window.innerHeight / 2
@ -118,10 +187,7 @@ const Graph = ({nodes, edges, render}) =>
{ {
layout.eachNode(({id}, {p}) => layout.eachNode(({id}, {p}) =>
{ {
const element = graphParent.current.querySelector( const element = findNode(graphParent.current, id);
`[data-node-id="${id.replace('"', '\\"')}"]`
);
const {x, y} = coordsToScreen(p); const {x, y} = coordsToScreen(p);
element.style.transform = `translate( element.style.transform = `translate(
@ -132,10 +198,7 @@ const Graph = ({nodes, edges, render}) =>
layout.eachEdge(({id}, {point1: {p: p1}, point2: {p: p2}}) => layout.eachEdge(({id}, {point1: {p: p1}, point2: {p: p2}}) =>
{ {
const element = graphParent.current.querySelector( const element = findEdge(graphParent.current, id);
`[data-edge-id="${id}"]`
);
const {x: x1, y: y1} = coordsToScreen(p1); const {x: x1, y: y1} = coordsToScreen(p1);
const {x: x2, y: y2} = coordsToScreen(p2); const {x: x2, y: y2} = coordsToScreen(p2);
@ -144,10 +207,6 @@ const Graph = ({nodes, edges, render}) =>
element.setAttribute('x2', x2); element.setAttribute('x2', x2);
element.setAttribute('y2', y2); element.setAttribute('y2', y2);
}); });
}, () => {
console.info(`Fin du rendu du graphe`);
}, () => {
console.info(`Démarrage du rendu du graphe`);
}); });
let dragging = null; let dragging = null;
@ -156,13 +215,12 @@ const Graph = ({nodes, edges, render}) =>
{ {
const {clientX: x, clientY: y} = ev; const {clientX: x, clientY: y} = ev;
const screen = new Springy.Vector(x, y); const screen = new Springy.Vector(x, y);
const position = screenToCoords(screen); const clickedNode = findParentNode(ev.target);
const nearest = layout.nearest(position);
if (nearest.distance <= scale / 50) if (clickedNode !== null)
{ {
dragging = nearest; dragging = layout.nodePoints[clickedNode];
dragging.point.m = 1000; dragging.m = Infinity;
} }
}; };
@ -172,7 +230,7 @@ const Graph = ({nodes, edges, render}) =>
{ {
const {clientX: x, clientY: y} = ev; const {clientX: x, clientY: y} = ev;
const screen = new Springy.Vector(x, y); const screen = new Springy.Vector(x, y);
dragging.point.p = screenToCoords(screen); dragging.p = screenToCoords(screen);
} }
}; };
@ -180,7 +238,7 @@ const Graph = ({nodes, edges, render}) =>
{ {
if (dragging !== null) if (dragging !== null)
{ {
dragging.point.m = 1; dragging.m = 1;
dragging = null; dragging = null;
} }
}; };
@ -195,20 +253,24 @@ const Graph = ({nodes, edges, render}) =>
document.body.removeEventListener('mousemove', mouseMove); document.body.removeEventListener('mousemove', mouseMove);
document.body.removeEventListener('mouseup', mouseUp); document.body.removeEventListener('mouseup', mouseUp);
}; };
}); }, []);
return ( return (
<div ref={graphParent} className="Graph"> <div ref={graphParent} className="Graph">
<svg className="Graph_edgesContainer"> <svg className="Graph_edgesContainer">
{edges.map(edge => ( {edges.map(edge => (
<line data-edge-id={getEdgeId(...edge)} /> <line
key={getEdgeId(...edge)}
data-edge-id={getEdgeId(...edge)}
/>
))} ))}
</svg> </svg>
{nodes.map(id => ( {nodes.map(id => (
<span <span
className="Graph_node" key={id}
data-node-id={id} data-node-id={id}
className="Graph_node"
>{render(id)}</span> >{render(id)}</span>
))} ))}
</div> </div>

View File

@ -46,6 +46,7 @@ const TermInput = ({terms, setTerms}) =>
<div className="TermInput"> <div className="TermInput">
{terms.map(term => {terms.map(term =>
<span <span
key={term}
className="TermInput_term" className="TermInput_term"
onClick={handleRemove.bind(null, term)} onClick={handleRemove.bind(null, term)}
>{term}</span> >{term}</span>