app: Nettoyage du modèle de données

This commit is contained in:
Mattéo Delabre 2019-12-04 18:54:44 -05:00
parent 5ad7ee0b55
commit 268289f1f7
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
11 changed files with 175 additions and 42 deletions

View File

@ -21,6 +21,11 @@
"plugins": [ "plugins": [
"react" "react"
], ],
"settings": {
"react": {
"version": "detect"
}
},
"rules": { "rules": {
"indent": [ "indent": [
"error", "error",
@ -37,6 +42,13 @@
"semi": [ "semi": [
"error", "error",
"always" "always"
],
"no-irregular-whitespace": [
"error",
{
"skipStrings": true,
"skipTemplates": true
}
] ]
} }
} }

View File

@ -5,7 +5,7 @@ import {useAsync} from '../util.js';
import { import {
diseasesBySymptoms, diseasesBySymptoms,
exploreSymptoms exploreSymptoms
} from '../data/fetch.js'; } from '../data/mock';
const App = () => const App = () =>
{ {

View File

@ -1,9 +1,18 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import Graph from './Graph.js'; import Graph from './Graph.js';
import {types} from '../data/mock.js';
import {useAsync} from '../util.js'; import {useAsync} from '../util.js';
import {symptomsSubgraph} from '../data/fetch.js'; import {Term, termTypes} from '../data/model.js';
import {symptomsSubgraph} from '../data/mock';
/**
* Graphe de maladies et symptômes.
*
* @prop terms Ensemble de symptômes recherchés par lutilisateur.
* @prop setTerms Fonction de rappel pour ajouter de nouveaux termes de
* recherche.
* @prop results Maladies correspondant à la recherche de lutilisateur.
*/
const DiseaseGraph = ({terms, setTerms, results}) => const DiseaseGraph = ({terms, setTerms, results}) =>
{ {
const {nodes, edges} = useAsync({ const {nodes, edges} = useAsync({
@ -19,7 +28,7 @@ const DiseaseGraph = ({terms, setTerms, results}) =>
const render = id => const render = id =>
{ {
const isTerm = terms.some(({id: termId}) => termId === id); const isTerm = terms.some(({id: termId}) => termId === id);
const isDisease = nodes[id].types.includes(types.disease); const isDisease = nodes[id].types.includes(termTypes.disease);
return ( return (
<span className={[ <span className={[
@ -69,4 +78,10 @@ const DiseaseGraph = ({terms, setTerms, results}) =>
); );
}; };
DiseaseGraph.propTypes = {
terms: PropTypes.arrayOf(Term).isRequired,
setTerms: PropTypes.func.isRequired,
results: PropTypes.arrayOf(Term).isRequired,
};
export default DiseaseGraph; export default DiseaseGraph;

View File

@ -1,6 +1,8 @@
import React, {useState, useRef, useEffect} from 'react'; import React, {useState, useRef, useEffect} from 'react';
import PropTypes from 'prop-types';
import {TransitionGroup, CSSTransition} from 'react-transition-group'; import {TransitionGroup, CSSTransition} from 'react-transition-group';
import Springy from 'springy'; import Springy from 'springy';
import {Relation} from '../data/model.js';
/** /**
* Échappe une valeur utilisée dans un sélecteur dattributs CSS. * Échappe une valeur utilisée dans un sélecteur dattributs CSS.
@ -343,4 +345,12 @@ const Graph = ({
); );
}; };
Graph.propTypes = {
nodes: PropTypes.arrayOf(PropTypes.any).isRequired,
edges: PropTypes.arrayOf(Relation).isRequired,
emptyMessage: PropTypes.string.isRequired,
render: PropTypes.func.isRequired,
onNodeClick: PropTypes.func.isRequired,
};
export default Graph; export default Graph;

View File

@ -1,4 +1,6 @@
import React, {useState, useRef} from 'react'; import React, {useState, useRef} from 'react';
import PropTypes from 'prop-types';
import {Term} from '../data/model.js';
import * as fuzzy from '../data/fuzzy.js'; import * as fuzzy from '../data/fuzzy.js';
import * as util from '../util.js'; import * as util from '../util.js';
@ -196,4 +198,10 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
); );
}; };
TermInput.propTypes = {
terms: PropTypes.arrayOf(Term).isRequired,
availableTerms: PropTypes.arrayOf(Term).isRequired,
setTerms: PropTypes.func.isRequired
};
export default TermInput; export default TermInput;

View File

@ -1,3 +1,8 @@
/**
* @module
* Fonctions permettant la recherche approximative de termes.
*/
import * as diacritics from 'diacritics'; import * as diacritics from 'diacritics';
/** /**

View File

@ -3,14 +3,7 @@
* Jeu de données dexemple. * Jeu de données dexemple.
*/ */
/** import {termTypes} from '../model.js';
* Types de termes de la base.
*/
export const types = {
disease: 'Maladie',
symptom: 'Symptôme',
sign: 'Signe',
};
/** /**
* Liste des termes de la base de données, contenant des maladies, des signes * Liste des termes de la base de données, contenant des maladies, des signes
@ -21,7 +14,7 @@ export const terms = {
id: 'Q2840', id: 'Q2840',
name: 'Grippe', name: 'Grippe',
alias: [], alias: [],
types: [types.disease], types: [termTypes.disease],
weight: 0.000035, weight: 0.000035,
}, },
Q154882: { Q154882: {
@ -30,14 +23,14 @@ export const terms = {
alias: [ alias: [
'Maladie des légionnaires', 'Maladie des légionnaires',
], ],
types: [types.disease], types: [termTypes.disease],
weight: 0.000015, weight: 0.000015,
}, },
Q155098: { Q155098: {
id: 'Q155098', id: 'Q155098',
name: 'Leptospirose', name: 'Leptospirose',
alias: [], alias: [],
types: [types.disease], types: [termTypes.disease],
weight: 0.00001, weight: 0.00001,
}, },
Q326663: { Q326663: {
@ -53,14 +46,14 @@ export const terms = {
'Maladie de Schneider', 'Maladie de Schneider',
'Maladie de Kumlinge', 'Maladie de Kumlinge',
], ],
types: [types.disease], types: [termTypes.disease],
weight: 0.000001, weight: 0.000001,
}, },
Q133780: { Q133780: {
id: 'Q133780', id: 'Q133780',
name: 'Peste', name: 'Peste',
alias: [], alias: [],
types: [types.disease], types: [termTypes.disease],
weight: 0.000032, weight: 0.000032,
}, },
Q38933: { Q38933: {
@ -69,7 +62,8 @@ export const terms = {
alias: [ alias: [
'Pyrexie', 'Pyrexie',
], ],
types: [types.symptom], types: [termTypes.symptom],
weight: 0,
}, },
Q474959: { Q474959: {
id: 'Q474959', id: 'Q474959',
@ -77,7 +71,8 @@ export const terms = {
alias: [ alias: [
'Douleur musculaire', 'Douleur musculaire',
], ],
types: [types.symptom], types: [termTypes.symptom],
weight: 0,
}, },
Q86: { Q86: {
id: 'Q86', id: 'Q86',
@ -85,7 +80,8 @@ export const terms = {
alias: [ alias: [
'Mal de tête', 'Mal de tête',
], ],
types: [types.sign], types: [termTypes.sign],
weight: 0,
}, },
Q1115038: { Q1115038: {
id: 'Q1115038', id: 'Q1115038',
@ -94,13 +90,15 @@ export const terms = {
'Nez qui coule', 'Nez qui coule',
'Écoulement nasal', 'Écoulement nasal',
], ],
types: [types.symptom], types: [termTypes.symptom],
weight: 0,
}, },
Q9690: { Q9690: {
id: 'Q9690', id: 'Q9690',
name: 'Fatigue', name: 'Fatigue',
alias: [], alias: [],
types: [types.symptom], types: [termTypes.symptom],
weight: 0,
}, },
Q127076: { Q127076: {
id: 'Q127076', id: 'Q127076',
@ -110,7 +108,8 @@ export const terms = {
'Vomissage', 'Vomissage',
'Vomir', 'Vomir',
], ],
types: [types.symptom, types.sign], types: [termTypes.symptom, termTypes.sign],
weight: 0,
}, },
Q178061: { Q178061: {
id: 'Q178061', id: 'Q178061',
@ -118,7 +117,7 @@ export const terms = {
alias: [ alias: [
'Insuffisance circulatoire aiguë', 'Insuffisance circulatoire aiguë',
], ],
types: [types.disease], types: [termTypes.disease],
weight: 0.000038, weight: 0.000038,
}, },
Q35805: { Q35805: {
@ -128,7 +127,8 @@ export const terms = {
'Tousse', 'Tousse',
'Toussote', 'Toussote',
], ],
types: [types.symptom, types.sign], types: [termTypes.symptom, termTypes.sign],
weight: 0,
}, },
Q647099: { Q647099: {
id: 'Q647099', id: 'Q647099',
@ -136,13 +136,15 @@ export const terms = {
alias: [ alias: [
'Expectoration sanglante', 'Expectoration sanglante',
], ],
types: [types.symptom], types: [termTypes.symptom],
weight: 0,
}, },
Q653197: { Q653197: {
id: 'Q653197', id: 'Q653197',
name: 'Rash', name: 'Rash',
alias: [], alias: [],
types: [types.symptom, types.sign], types: [termTypes.symptom, termTypes.sign],
weight: 0,
}, },
Q160796: { Q160796: {
id: 'Q160796', id: 'Q160796',
@ -150,28 +152,29 @@ export const terms = {
alias: [ alias: [
'Confusion mentale', 'Confusion mentale',
], ],
types: [types.disease], types: [termTypes.disease],
weight: 0.000004, weight: 0.000004,
}, },
Q186235: { Q186235: {
id: 'Q186235', id: 'Q186235',
name: 'Myocardite', name: 'Myocardite',
alias: [], alias: [],
types: [types.disease], types: [termTypes.disease],
weight: 0.0000075, weight: 0.0000075,
}, },
Q476921: { Q476921: {
id: 'Q476921', id: 'Q476921',
name: 'Insuffisance rénale', name: 'Insuffisance rénale',
alias: [], alias: [],
types: [types.disease], types: [termTypes.disease],
weight: 0.0000046, weight: 0.0000046,
}, },
Q281289: { Q281289: {
id: 'Q281289', id: 'Q281289',
name: 'Photophobie', name: 'Photophobie',
alias: [], alias: [],
types: [types.sign], types: [termTypes.sign],
weight: 0,
}, },
Q159557: { Q159557: {
id: 'Q159557', id: 'Q159557',
@ -181,7 +184,8 @@ export const terms = {
'Coma végétatif', 'Coma végétatif',
'Perte de connaissance', 'Perte de connaissance',
], ],
types: [types.sign], types: [termTypes.sign],
weight: 0,
}, },
}; };

View File

@ -1,4 +1,10 @@
import * as mock from './mock.js'; /**
* @module
* Fonctions de requêtage des données dexemple.
*/
import {termTypes} from '../model.js';
import * as data from './data.js';
/** /**
* Recherche lensemble des maladies liées par une relation « a pour symptôme » * Recherche lensemble des maladies liées par une relation « a pour symptôme »
@ -14,7 +20,7 @@ export const diseasesBySymptoms = async query =>
if (!query.length) if (!query.length)
{ {
// Si aucun terme dans la requête, tout correspond // Si aucun terme dans la requête, tout correspond
allMatches = Object.values(mock.terms).map(({id}) => id); allMatches = Object.values(data.terms).map(({id}) => id);
} }
else else
{ {
@ -32,7 +38,7 @@ export const diseasesBySymptoms = async query =>
while (stack.length) while (stack.length)
{ {
const current = stack.pop(); const current = stack.pop();
const neighbors = mock.symptomOf.filter( const neighbors = data.symptomOf.filter(
([from]) => from === current ([from]) => from === current
).map( ).map(
([, to]) => to ([, to]) => to
@ -65,9 +71,9 @@ export const diseasesBySymptoms = async query =>
// On ne garde que les maladies // On ne garde que les maladies
return allMatches.map( return allMatches.map(
id => mock.terms[id] id => data.terms[id]
).filter( ).filter(
term => term.types.includes(mock.types.disease) term => term.types.includes(termTypes.disease)
); );
}; };
@ -91,7 +97,7 @@ export const exploreSymptoms = async terms =>
while (stack.length) while (stack.length)
{ {
const current = stack.pop(); const current = stack.pop();
const neighbors = mock.hasSymptom.filter( const neighbors = data.hasSymptom.filter(
([from]) => from === current ([from]) => from === current
).map( ).map(
([, to]) => to ([, to]) => to
@ -107,7 +113,7 @@ export const exploreSymptoms = async terms =>
} }
} }
return selected.map(id => mock.terms[id]); return selected.map(id => data.terms[id]);
}; };
/** /**
@ -131,7 +137,7 @@ export const symptomsSubgraph = async terms =>
} }
// Sélection des arêtes liant les nœuds sélectionnés // Sélection des arêtes liant les nœuds sélectionnés
for (let [from, to] of mock.hasSymptom) for (let [from, to] of data.hasSymptom)
{ {
if ( if (
termsIds.includes(from) termsIds.includes(from)

72
app/src/data/model.js Normal file
View File

@ -0,0 +1,72 @@
/**
* @module
* Définit le modèle de données utilisé par lapplication.
*/
import PropTypes from 'prop-types';
/**
* Types de termes existants.
*/
export const termTypes = {
disease: 'Maladie',
symptom: 'Symptôme',
sign: 'Signe',
};
/**
* Type de terme.
*/
export const TermType = PropTypes.oneOf(Object.values(termTypes));
/**
* Terme.
*
* Peut être une maladie, un symptôme ou un signe.
*/
export const Term = PropTypes.exact({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
alias: PropTypes.arrayOf(PropTypes.string).isRequired,
types: PropTypes.arrayOf(TermType).isRequired,
weight: PropTypes.number.isRequired,
});
/**
* Relation entre deux termes.
*/
export const Relation = (props, propName, componentName) =>
{
if (!(propName in props))
{
return new Error(
`Missing ${propName} in props supplied to ${componentName}.`
);
}
const value = props[propName];
if (!Array.isArray(value))
{
return new Error(
`Relation ${propName} must be an array.`
);
}
if (value.length !== 2)
{
return new Error(
`Relation ${propName} must contain exactly two entries.`
);
}
if (
typeof value[0] !== 'string'
|| typeof value[1] !== 'string'
)
{
return new Error(
`Entries of relation ${propName} must be string IDs.`
);
}
};

View File

@ -28,7 +28,7 @@ export const useAsync = (initial, func, ...args) =>
/** /**
* Crée un état composé dune liste et dun élément ayant le focus dans cette * Crée un état composé dune liste et dun élément ayant le focus dans cette
* liste. À la modification de la liste ou de lindice de lélément ayant le * liste. À la modification de la liste ou de lindice de lélément ayant le
* focus, la contrainte suivante est imposée: * focus, la contrainte suivante est imposée :
* *
* si la liste nest pas vide, focus [0, taille de la liste]. * si la liste nest pas vide, focus [0, taille de la liste].
* sinon, focus = 0. * sinon, focus = 0.

View File

@ -21,6 +21,7 @@
"diacritics": "^1.3.0", "diacritics": "^1.3.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-react": "^7.17.0", "eslint-plugin-react": "^7.17.0",
"prop-types": "^15.7.2",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-transition-group": "^4.3.0", "react-transition-group": "^4.3.0",