Merge branch 'master' of gitlab.com:matteodelabre/wikimedica-disease-search

This commit is contained in:
Rémi Cérès 2019-11-27 04:16:25 -05:00
commit 69c96ac537
12 changed files with 188 additions and 41 deletions

View File

@ -1,3 +1,4 @@
{ {
"presets": ["@babel/preset-env", "@babel/preset-react"] "presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-transform-runtime"]
} }

View File

@ -9,6 +9,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="index.js"></script> <script src="src/index.js"></script>
</body> </body>
</html> </html>

View File

@ -1,6 +1,6 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import TermInput from './TermInput.js'; import TermInput from './TermInput.js';
import ResultsGraph from './ResultsGraph.js'; import SearchResults from './SearchResults.js';
const App = () => const App = () =>
{ {
@ -9,7 +9,7 @@ const App = () =>
return ( return (
<div className="App"> <div className="App">
<TermInput terms={terms} setTerms={setTerms} /> <TermInput terms={terms} setTerms={setTerms} />
<ResultsGraph /> <SearchResults terms={terms} />
</div> </div>
); );
}; };

View File

@ -191,7 +191,6 @@ const Graph = ({nodes, edges, render}) =>
return () => return () =>
{ {
layout.stop();
graphParent.current.removeEventListener('mousedown', mouseDown); graphParent.current.removeEventListener('mousedown', mouseDown);
document.body.removeEventListener('mousemove', mouseMove); document.body.removeEventListener('mousemove', mouseMove);
document.body.removeEventListener('mouseup', mouseUp); document.body.removeEventListener('mouseup', mouseUp);

13
app/src/ResultsGraph.js Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
import Graph from './Graph.js';
const ResultsGraph = () => (
<Graph
nodes={Object.keys(nodes)}
edges={edges}
render={id => nodes[id].name}
/>
);
export default ResultsGraph;

37
app/src/SearchResults.js Normal file
View File

@ -0,0 +1,37 @@
import React, {useState, useEffect} from 'react';
import Graph from './Graph.js';
import {searchTerms} from './fetch.js';
const useResults = terms =>
{
const [results, setResults] = useState({
nodes: {},
edges: []
});
useEffect(() =>
{
const fetch = async () =>
{
setResults(await searchTerms(terms));
};
fetch();
}, [terms]);
return results;
};
const SearchResults = ({terms}) =>
{
const {nodes, edges} = useResults(terms);
return (
<Graph
nodes={Object.keys(nodes)}
edges={edges}
render={id => nodes[id].name}
/>
);
};
export default SearchResults;

View File

@ -1,6 +1,7 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
const enterKey = 13; const enterKey = 13;
const backspaceKey = 8;
const TermInput = ({terms, setTerms}) => const TermInput = ({terms, setTerms}) =>
{ {
@ -8,7 +9,7 @@ const TermInput = ({terms, setTerms}) =>
const handleKeyDown = ev => const handleKeyDown = ev =>
{ {
if (ev.keyCode === enterKey) if (ev.keyCode === enterKey && value)
{ {
ev.preventDefault(); ev.preventDefault();
@ -18,6 +19,14 @@ const TermInput = ({terms, setTerms}) =>
} }
setValue(''); setValue('');
return;
}
if (ev.keyCode === backspaceKey && !value)
{
ev.preventDefault();
setTerms(terms.slice(0, -1));
return;
} }
}; };
@ -42,7 +51,7 @@ const TermInput = ({terms, setTerms}) =>
>{term}</span> >{term}</span>
)} )}
<input <input
autofocus="true" type="text" className="TermInput_input" autoFocus={true} type="text" className="TermInput_input"
placeholder="Rechercher un symptôme, un signe ou une maladie…" placeholder="Rechercher un symptôme, un signe ou une maladie…"
value={value} value={value}
onChange={handleChange} onKeyDown={handleKeyDown} /> onChange={handleChange} onKeyDown={handleKeyDown} />

View File

@ -1,21 +1,4 @@
import React from 'react'; const mockNodes = {
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: { Q2840: {
id: 'Q2840', id: 'Q2840',
name: 'Grippe', name: 'Grippe',
@ -127,7 +110,7 @@ const nodes = {
} }
}; };
const edges = [ const mockEdges = [
['Q2840', 'Q38933'], ['Q2840', 'Q38933'],
['Q2840', 'Q1115038'], ['Q2840', 'Q1115038'],
['Q2840', 'Q474959'], ['Q2840', 'Q474959'],
@ -165,12 +148,83 @@ const edges = [
['Q133780', 'Q160796'] ['Q133780', 'Q160796']
]; ];
const ResultsGraph = () => ( /**
<Graph * Vérifie si une liste darêtes contient une arête donnée, dans un sens ou
nodes={Object.keys(nodes)} * dans lautre.
edges={edges} *
render={id => nodes[id].name} * @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)
));
export default ResultsGraph; export const searchTerms = terms => new Promise((res, rej) =>
{
// Fait attendre artificiellement pour simuler une requête
setTimeout(() =>
{
const nodes = {};
const edges = [];
// Récupération des identifiants correspondant aux termes de la requête
const termIds = terms.map(term =>
Object.keys(mockNodes)
.find(id => mockNodes[id].name === term)
);
// Si lun des termes est inconnu, aucun résultat
if (!termIds.includes(undefined))
{
// Sélection des termes de la requête
for (let termId of termIds)
{
nodes[termId] = mockNodes[termId];
}
// Sélection des nœuds liés à tous les éléments de la requête
const resultIds = [];
for (let [id, data] of Object.entries(mockNodes))
{
if (termIds.every(
termId => includesEdge(mockEdges, id, termId)
))
{
nodes[id] = data;
resultIds.push(id);
}
}
// Sélection des voisins des résultats de la requête
for (let [id, data] of Object.entries(mockNodes))
{
for (let resultId of resultIds)
{
if (includesEdge(mockEdges, resultId, id))
{
nodes[id] = data;
}
}
}
// Sélection des arêtes liant les nœuds sélectionnés
for (let [from, to] of mockEdges)
{
if (
(from in nodes && to in nodes)
&& !includesEdge(edges, from, to)
)
{
edges.push([from, to]);
}
}
}
res({nodes, edges});
}, 500);
});

View File

@ -104,7 +104,7 @@ input
color: var(--color-light); color: var(--color-light);
border-radius: 2px; border-radius: 2px;
padding: calc(.2 * var(--base-size)); padding: calc(.25 * var(--base-size)) calc(.5 * var(--base-size));
margin-left: calc(.3 * var(--base-size)); margin-left: calc(.3 * var(--base-size));
cursor: pointer; cursor: pointer;

44
package-lock.json generated
View File

@ -730,6 +730,40 @@
"@babel/helper-plugin-utils": "^7.0.0" "@babel/helper-plugin-utils": "^7.0.0"
} }
}, },
"@babel/plugin-transform-runtime": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.4.tgz",
"integrity": "sha512-O8kSkS5fP74Ad/8pfsCMGa8sBRdLxYoSReaARRNSz3FbFQj3z/QUvoUmJ28gn9BO93YfnXc3j+Xyaqe8cKDNBQ==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.7.4",
"@babel/helper-plugin-utils": "^7.0.0",
"resolve": "^1.8.1",
"semver": "^5.5.1"
},
"dependencies": {
"@babel/helper-module-imports": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.7.4.tgz",
"integrity": "sha512-dGcrX6K9l8258WFjyDLJwuVKxR4XZfU0/vTUgOQYWEnRD8mgr+p4d6fCUMq/ys0h4CCt/S5JhbvtyErjWouAUQ==",
"dev": true,
"requires": {
"@babel/types": "^7.7.4"
}
},
"@babel/types": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz",
"integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
}
}
},
"@babel/plugin-transform-shorthand-properties": { "@babel/plugin-transform-shorthand-properties": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz",
@ -860,10 +894,9 @@
} }
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.7.2", "version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz",
"integrity": "sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==", "integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==",
"dev": true,
"requires": { "requires": {
"regenerator-runtime": "^0.13.2" "regenerator-runtime": "^0.13.2"
} }
@ -5954,8 +5987,7 @@
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.13.3", "version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
"dev": true
}, },
"regenerator-transform": { "regenerator-transform": {
"version": "0.14.1", "version": "0.14.1",

View File

@ -5,11 +5,13 @@
"main": "index.js", "main": "index.js",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.2", "@babel/core": "^7.7.2",
"@babel/plugin-transform-runtime": "^7.7.4",
"@babel/preset-env": "^7.7.1", "@babel/preset-env": "^7.7.1",
"@babel/preset-react": "^7.7.0", "@babel/preset-react": "^7.7.0",
"parcel-bundler": "^1.12.4" "parcel-bundler": "^1.12.4"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.7.4",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"springy": "^2.8.0" "springy": "^2.8.0"