app: Améliorations de performance
This commit is contained in:
parent
2efc32c5a2
commit
4295a94edf
160
app/src/Graph.js
160
app/src/Graph.js
|
@ -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 d’attributs CSS.
|
||||||
|
*
|
||||||
|
* @param value Valeur à échapper.
|
||||||
|
* @return Valeur échappée.
|
||||||
|
*/
|
||||||
|
const escapeAttrValue = value => value.replace(/"/g, '\\"');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée un identifiant unique pour l’arête identifiée par ses deux extrémités.
|
* Crée un identifiant unique pour l’arê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 d’un élément celui qui représente un
|
||||||
|
* nœud donnée, s’il 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 d’un élément celui qui représente une
|
||||||
|
* arête donnée, s’il existe.
|
||||||
|
*
|
||||||
|
* @param parent Élément parent.
|
||||||
|
* @param id Identifiant de l’arê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
|
||||||
|
* l’identifiant 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 d’identifiants de nœuds formant les arêtes du graphe.
|
* @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
|
* @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.
|
* 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
|
||||||
));
|
));
|
||||||
|
|
||||||
// N’arrête jamais l’animation
|
// N’arrête jamais l’animation
|
||||||
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 l’ancien 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 l’ancienne 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 l’animation 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue