Merge branch 'master' of gitlab.com:matteodelabre/wikimedica-disease-search
This commit is contained in:
		
						commit
						67405c9004
					
				
							
								
								
									
										12
									
								
								README.md
								
								
								
								
							
							
						
						
									
										12
									
								
								README.md
								
								
								
								
							|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										16
									
								
								app/App.js
								
								
								
								
							
							
						
						
									
										16
									
								
								app/App.js
								
								
								
								
							|  | @ -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; | ||||
|  | @ -2,7 +2,10 @@ | |||
| <html> | ||||
|     <head> | ||||
|         <meta charset="utf-8"> | ||||
|         <title>Bonjour</title> | ||||
|         <title>Recherche de maladies par symptômes</title> | ||||
| 
 | ||||
|         <link rel="stylesheet" href="style/font/SourceSansPro.css"> | ||||
|         <link rel="stylesheet" href="style/style.css"> | ||||
|     </head> | ||||
|     <body> | ||||
|         <div id="root"></div> | ||||
|  |  | |||
										
											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; | ||||
| } | ||||
|  | @ -6561,6 +6561,11 @@ | |||
|         "extend-shallow": "^3.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "springy": { | ||||
|       "version": "2.8.0", | ||||
|       "resolved": "https://registry.npmjs.org/springy/-/springy-2.8.0.tgz", | ||||
|       "integrity": "sha512-PXtwxjww53H/8c+ng3zxMJwNRN/KPv6DKA8ITHi6lW57gfzty0wV/N8ZeTmgfO0ElfQip8W/1iVZk1d5ne4L+g==" | ||||
|     }, | ||||
|     "sprintf-js": { | ||||
|       "version": "1.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ | |||
|   }, | ||||
|   "dependencies": { | ||||
|     "react": "^16.12.0", | ||||
|     "react-dom": "^16.12.0" | ||||
|     "react-dom": "^16.12.0", | ||||
|     "springy": "^2.8.0" | ||||
|   } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue