Rémi Cérès
4 years ago
16 changed files with 668 additions and 14 deletions
@ -1,15 +1,7 @@ |
|||
Liste des signes, symptômes et maladies : |
|||
|
|||
https://wikimedi.ca/wiki/Concept:Signes_et_sympt%C3%B4mes |
|||
<https://wikimedi.ca/wiki/Concept:Signes_et_sympt%C3%B4mes> |
|||
|
|||
Export RDF : |
|||
|
|||
https://wikimedi.ca/wiki/Sp%C3%A9cial:Export_RDF/<NOM_DE_LA_PAGE> |
|||
|
|||
Visu : |
|||
|
|||
* https://js.cytoscape.org/ |
|||
* https://www.npmjs.com/package/react-graph-vis |
|||
* http://sigmajs.org/ |
|||
* http://getspringy.com/ |
|||
* https://github.com/danielcaldas/react-d3-graph |
|||
<https://wikimedi.ca/wiki/Sp%C3%A9cial:Export_RDF/NOM_DE_LA_PAGE> |
|||
|
@ -1,5 +1,17 @@ |
|||
import React from 'react'; |
|||
import React, {useState} from 'react'; |
|||
import TermInput from './TermInput.js'; |
|||
import ResultsGraph from './ResultsGraph.js'; |
|||
|
|||
const App = () => <div>Je suis rechargé !</div>; |
|||
const App = () => |
|||
{ |
|||
const [terms, setTerms] = useState([]); |
|||
|
|||
return ( |
|||
<div className="App"> |
|||
<TermInput terms={terms} setTerms={setTerms} /> |
|||
<ResultsGraph /> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default App; |
|||
|
@ -0,0 +1,219 @@ |
|||
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; |
@ -0,0 +1,176 @@ |
|||
import React from 'react'; |
|||
import Graph from './Graph.js'; |
|||
|
|||
// const config = {
|
|||
// automaticRearrangeAfterDropNode: true,
|
|||
// node: {
|
|||
// color: 'lightgreen',
|
|||
// size: 240,
|
|||
// fontSize: 14,
|
|||
// highlightStrokeColor: 'blue',
|
|||
// labelProperty: 'name'
|
|||
// },
|
|||
// link: {
|
|||
// highlightColor: 'lightblue'
|
|||
// }
|
|||
// };
|
|||
|
|||
const nodes = { |
|||
Q2840: { |
|||
id: 'Q2840', |
|||
name: 'Grippe', |
|||
types: ['Maladie'], |
|||
weight: 0.000035 |
|||
}, |
|||
Q154882: { |
|||
id: 'Q154882', |
|||
name: 'Légionellose', |
|||
types: ['Maladie'], |
|||
weight: 0.000015 |
|||
}, |
|||
Q155098: { |
|||
id: 'Q155098', |
|||
name: 'Leptospirose', |
|||
types: ['Maladie'], |
|||
weight: 0.00001 |
|||
}, |
|||
Q326663: { |
|||
id: 'Q326663', |
|||
name: 'Encéphalite à tiques', |
|||
types: ['Maladie'], |
|||
weight: 0.000001 |
|||
}, |
|||
Q133780: { |
|||
id: 'Q133780', |
|||
name: 'Peste', |
|||
types: ['Maladie'], |
|||
weight: 0.000032 |
|||
}, |
|||
Q38933: { |
|||
id: 'Q38933', |
|||
name: 'Fièvre', |
|||
types: ['Symptôme'] |
|||
}, |
|||
Q474959: { |
|||
id: 'Q474959', |
|||
name: 'Myalgie', |
|||
types: ['Symptôme'] |
|||
}, |
|||
Q86: { |
|||
id: 'Q86', |
|||
name: 'Céphalée', |
|||
types: ['Signe'] |
|||
}, |
|||
Q1115038: { |
|||
id: 'Q1115038', |
|||
name: 'Rhinorrhée', |
|||
types: ['Symptôme'] |
|||
}, |
|||
Q9690: { |
|||
id: 'Q9690', |
|||
name: 'Fatigue', |
|||
types: ['Symptôme'] |
|||
}, |
|||
Q127076: { |
|||
id: 'Q127076', |
|||
name: 'Vomissement', |
|||
types: ['Symptôme', 'Signe'] |
|||
}, |
|||
Q178061: { |
|||
id: 'Q178061', |
|||
name: 'Choc circulatoire', |
|||
types: ['Maladie'], |
|||
weight: 0.000038 |
|||
}, |
|||
Q35805: { |
|||
id: 'Q35805', |
|||
name: 'Toux', |
|||
types: ['Symptôme', 'Signe'] |
|||
}, |
|||
Q647099: { |
|||
id: 'Q647099', |
|||
name: 'Hémoptysie', |
|||
types: ['Symptôme'] |
|||
}, |
|||
Q653197: { |
|||
id: 'Q653197', |
|||
name: 'Rash', |
|||
types: ['Symptôme', 'Signe'] |
|||
}, |
|||
Q160796: { |
|||
id: 'Q160796', |
|||
name: 'Syndrome confusionnel', |
|||
types: ['Maladie'], |
|||
weight: 0.000004 |
|||
}, |
|||
Q186235: { |
|||
id: 'Q186235', |
|||
name: 'Myocardite', |
|||
types: ['Maladie'], |
|||
weight: 0.0000075 |
|||
}, |
|||
Q476921: { |
|||
id: 'Q476921', |
|||
name: 'Insuffisance rénale', |
|||
types: ['Maladie'], |
|||
weight: 0.0000046 |
|||
}, |
|||
Q281289: { |
|||
id: 'Q281289', |
|||
name: 'Photophobie', |
|||
types: ['Signe'] |
|||
}, |
|||
Q159557: { |
|||
id: 'Q159557', |
|||
name: 'Coma', |
|||
types: ['Signe'] |
|||
} |
|||
}; |
|||
|
|||
const edges = [ |
|||
['Q2840', 'Q38933'], |
|||
['Q2840', 'Q1115038'], |
|||
['Q2840', 'Q474959'], |
|||
['Q2840', 'Q86'], |
|||
['Q2840', 'Q9690'], |
|||
|
|||
['Q154882', 'Q86'], |
|||
['Q154882', 'Q38933'], |
|||
['Q154882', 'Q35805'], |
|||
['Q154882', 'Q474959'], |
|||
|
|||
['Q155098', 'Q38933'], |
|||
['Q155098', 'Q474959'], |
|||
['Q155098', 'Q86'], |
|||
['Q155098', 'Q476921'], |
|||
['Q155098', 'Q186235'], |
|||
['Q155098', 'Q653197'], |
|||
|
|||
['Q326663', 'Q86'], |
|||
['Q326663', 'Q474959'], |
|||
['Q326663', 'Q38933'], |
|||
['Q326663', 'Q9690'], |
|||
['Q326663', 'Q281289'], |
|||
['Q326663', 'Q159557'], |
|||
['Q326663', 'Q127076'], |
|||
|
|||
['Q133780', 'Q38933'], |
|||
['Q133780', 'Q86'], |
|||
['Q133780', 'Q127076'], |
|||
['Q133780', 'Q474959'], |
|||
['Q133780', 'Q178061'], |
|||
['Q133780', 'Q35805'], |
|||
['Q133780', 'Q647099'], |
|||
['Q133780', 'Q653197'], |
|||
['Q133780', 'Q160796'] |
|||
]; |
|||
|
|||
const ResultsGraph = () => ( |
|||
<Graph |
|||
nodes={Object.keys(nodes)} |
|||
edges={edges} |
|||
render={id => nodes[id].name} |
|||
/> |
|||
); |
|||
|
|||
export default ResultsGraph; |
@ -0,0 +1,53 @@ |
|||
import React, {useState} from 'react'; |
|||
|
|||
const enterKey = 13; |
|||
|
|||
const TermInput = ({terms, setTerms}) => |
|||
{ |
|||
const [value, setValue] = useState(''); |
|||
|
|||
const handleKeyDown = ev => |
|||
{ |
|||
if (ev.keyCode === enterKey) |
|||
{ |
|||
ev.preventDefault(); |
|||
|
|||
if (!terms.includes(value)) |
|||
{ |
|||
setTerms(terms.concat([value])); |
|||
} |
|||
|
|||
setValue(''); |
|||
} |
|||
}; |
|||
|
|||
const handleChange = ev => |
|||
{ |
|||
setValue(ev.target.value); |
|||
}; |
|||
|
|||
const handleRemove = removedTerm => |
|||
{ |
|||
console.log(removedTerm); |
|||
console.log(terms); |
|||
setTerms(terms.filter(term => term !== removedTerm)); |
|||
}; |
|||
|
|||
return ( |
|||
<div className="TermInput"> |
|||
{terms.map(term => |
|||
<span |
|||
className="TermInput_term" |
|||
onClick={handleRemove.bind(null, term)} |
|||
>{term}</span> |
|||
)} |
|||
<input |
|||
autofocus="true" type="text" className="TermInput_input" |
|||
placeholder="Rechercher un symptôme, un signe ou une maladie…" |
|||
value={value} |
|||
onChange={handleChange} onKeyDown={handleKeyDown} /> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default TermInput; |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,27 @@ |
|||
@font-face { |
|||
font-family: 'Source Sans Pro'; |
|||
src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), |
|||
url('SourceSansPro-Semibold.woff2') format('woff2'), |
|||
url('SourceSansPro-Semibold.woff') format('woff'); |
|||
font-weight: 600; |
|||
font-style: normal; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Source Sans Pro'; |
|||
src: local('Source Sans Pro Italic'), local('SourceSansPro-It'), |
|||
url('SourceSansPro-It.woff2') format('woff2'), |
|||
url('SourceSansPro-It.woff') format('woff'); |
|||
font-weight: normal; |
|||
font-style: italic; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Source Sans Pro'; |
|||
src: local('Source Sans Pro'), local('SourceSansPro-Regular'), |
|||
url('SourceSansPro-Regular.woff2') format('woff2'), |
|||
url('SourceSansPro-Regular.woff') format('woff'); |
|||
font-weight: normal; |
|||
font-style: normal; |
|||
} |
|||
|
@ -0,0 +1,166 @@ |
|||
:root |
|||
{ |
|||
--color-accent: #D80C49; |
|||
--color-secondary: #AE1246; |
|||
--color-light: #EEEEEE; |
|||
--color-dark: #1D1D1D; |
|||
|
|||
--font-family: 'Source Sans Pro'; |
|||
--font-size: 18px; |
|||
--font-color: var(--color-dark); |
|||
|
|||
--base-size: 25px; |
|||
|
|||
--animation-ease-out: cubic-bezier(0.075, 0.82, 0.165, 1); |
|||
--animation-short: .3s; |
|||
} |
|||
|
|||
body, html |
|||
{ |
|||
padding: 0; |
|||
margin: 0; |
|||
outline: 0; |
|||
height: 100%; |
|||
|
|||
background-color: var(--color-light); |
|||
|
|||
font-family: var(--font-family); |
|||
font-size: var(--font-size); |
|||
line-height: var(--base-size); |
|||
color: var(--font-color); |
|||
} |
|||
|
|||
body, html, #root |
|||
{ |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
input |
|||
{ |
|||
font-family: var(--font-family); |
|||
font-size: var(--font-size); |
|||
|
|||
padding: 0 calc(.6 * var(--base-size)); |
|||
height: calc(2 * var(--base-size)); |
|||
} |
|||
|
|||
*, *::before, *::after |
|||
{ |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
/** |
|||
* App |
|||
*/ |
|||
|
|||
.App |
|||
{ |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.App .TermInput |
|||
{ |
|||
position: absolute; |
|||
z-index: 2; |
|||
|
|||
width: 600px; |
|||
|
|||
top: calc(2 * var(--base-size)); |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
} |
|||
|
|||
/** |
|||
* TermInput |
|||
*/ |
|||
|
|||
.TermInput |
|||
{ |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
border: 2px solid var(--color-dark); |
|||
border-radius: 2px; |
|||
background: white; |
|||
|
|||
transition: box-shadow var(--animation-short) var(--animation-ease-out); |
|||
} |
|||
|
|||
.TermInput:focus-within |
|||
{ |
|||
box-shadow: |
|||
-1px -1px 0px var(--color-dark), |
|||
1px -1px 0px var(--color-dark), |
|||
1px 1px 0px var(--color-dark), |
|||
-1px 1px 0px var(--color-dark); |
|||
} |
|||
|
|||
.TermInput_term |
|||
{ |
|||
display: inline-block; |
|||
background: var(--color-secondary); |
|||
color: var(--color-light); |
|||
|
|||
border-radius: 2px; |
|||
padding: calc(.2 * var(--base-size)); |
|||
margin-left: calc(.3 * var(--base-size)); |
|||
cursor: pointer; |
|||
|
|||
transition: background var(--animation-short) var(--animation-ease-out); |
|||
} |
|||
|
|||
.TermInput_term:hover |
|||
{ |
|||
background: var(--color-accent); |
|||
} |
|||
|
|||
.TermInput_input |
|||
{ |
|||
flex: 1; |
|||
border: none; |
|||
} |
|||
|
|||
/** |
|||
* Graph |
|||
*/ |
|||
|
|||
.Graph |
|||
{ |
|||
position: relative; |
|||
display: block; |
|||
width: 100%; |
|||
height: 100%; |
|||
|
|||
overflow: hidden; |
|||
user-select: none; |
|||
} |
|||
|
|||
.Graph_edgesContainer |
|||
{ |
|||
position: absolute; |
|||
z-index: 0; |
|||
|
|||
top: 0; |
|||
left 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.Graph_edgesContainer line |
|||
{ |
|||
stroke: var(--color-dark); |
|||
} |
|||
|
|||
.Graph_node |
|||
{ |
|||
position: absolute; |
|||
z-index: 1; |
|||
|
|||
display: block; |
|||
transform: translate(-50%, -50%); |
|||
|
|||
background: white; |
|||
padding: 4px 8px; |
|||
} |
Loading…
Reference in new issue