UNPKG

auspice

Version:

Web app for visualizing pathogen evolution

219 lines (199 loc) 6.31 kB
import React from "react"; import { isValueValid } from "../../../util/globals"; import { infoPanelStyles } from "../../../globalStyles"; import { numericToCalendar } from "../../../util/dateHelpers"; import { getTraitFromNode, getFullAuthorInfoFromNode, getVaccineFromNode, getAccessionFromNode, getUrlFromNode } from "../../../util/treeMiscHelpers"; export const styles = { container: { position: "absolute", width: "100%", height: "100%", pointerEvents: "all", top: 0, left: 0, zIndex: 2000, backgroundColor: "rgba(80, 80, 80, .20)", /* FLEXBOX */ display: "flex", justifyContent: "center", alignItems: "center", wordWrap: "break-word", wordBreak: "break-word" } }; export const stopProp = (e) => { if (!e) {e = window.event;} // eslint-disable-line no-param-reassign e.cancelBubble = true; if (e.stopPropagation) {e.stopPropagation();} }; /* min width to get "collection date" on 1 line: 120 */ const item = (key, value, href) => ( <tr key={key}> <th style={infoPanelStyles.item}>{key}</th> <td style={infoPanelStyles.item}>{href ? ( <a href={href} target="_blank" rel="noopener noreferrer">{value}</a> ) : value }</td> </tr> ); const formatURL = (url) => { if (url !== undefined && url.startsWith("https_")) { return url.replace("https_", "https:"); } else if (url !== undefined && url.startsWith("http_")) { return url.replace("http_", "http:"); } return url; }; const AccessionAndUrl = ({node}) => { const accession = getAccessionFromNode(node); const url = getUrlFromNode(node); if (isValueValid(accession) && isValueValid(url)) { return ( <tr> <th style={infoPanelStyles.item}>Accession</th> <td style={infoPanelStyles.item}> <a href={formatURL(url)} target="_blank">{accession}</a> </td> </tr> ); } else if (isValueValid(accession)) { return ( item("Accession", accession) ); } else if (isValueValid(url)) { return ( <tr> <th style={infoPanelStyles.item}>Strain URL</th> <td style={infoPanelStyles.item}> <a href={formatURL(url)} target="_blank"><em>click here</em></a> </td> </tr> ); } return null; }; const VaccineInfo = ({node}) => { const vaccineInfo = getVaccineFromNode(node); if (!vaccineInfo) return null; const renderElements = []; if (vaccineInfo.selection_date) { renderElements.push( <tr key={"seldate"}> <th>Vaccine selected</th> <td>{vaccineInfo.selection_date}</td> </tr> ); } if (vaccineInfo.start_date) { renderElements.push( <tr key={"startdate"}> <th>Vaccine start date</th> <td>{vaccineInfo.start_date}</td> </tr> ); } if (vaccineInfo.end_date) { renderElements.push( <tr key={"enddate"}> <th>Vaccine end date</th> <td>{vaccineInfo.end_date}</td> </tr> ); } if (vaccineInfo.serum) { renderElements.push( <tr key={"serum"}> <th>Serum strain</th> <td/> </tr> ); } return renderElements; }; const PublicationInfo = ({node}) => { const info = getFullAuthorInfoFromNode(node); if (!info) return null; const itemsToRender = []; itemsToRender.push(item("Authors", info.value)); if (info.title && info.title !== "?") { if (info.paper_url && info.paper_url !== "?") { itemsToRender.push(item("Title", info.title, info.paper_url)); } else { itemsToRender.push(item("Title", info.title)); } } if (info.journal && info.journal !== "?") { itemsToRender.push(item("Journal", info.journal)); } return (itemsToRender.length === 1 ? itemsToRender[0] : itemsToRender); }; const StrainName = ({children}) => ( <p style={infoPanelStyles.modalHeading}>{children}</p> ); const SampleDate = ({node}) => { const date = getTraitFromNode(node, "num_date"); if (!date) return null; const dateUncertainty = getTraitFromNode(node, "num_date", {confidence: true}); if (date && dateUncertainty && dateUncertainty[0] !== dateUncertainty[1]) { return ( <> {item("Inferred collection date", numericToCalendar(date))} {item("Collection date confidence", `(${numericToCalendar(dateUncertainty[0])}, ${numericToCalendar(dateUncertainty[1])})`)} </> ); } return item("Collection date", numericToCalendar(date)); }; const getTraitsToDisplay = (node) => { // TODO -- this should be centralised somewhere if (!node.node_attrs) return []; const ignore = ["author", "div", "num_date"]; return Object.keys(node.node_attrs).filter((k) => !ignore.includes(k)); }; const Trait = ({node, trait, colorings}) => { const value_tmp = getTraitFromNode(node, trait); let value = value_tmp; if (typeof value_tmp == "number"){ if (!Number.isInteger(value_tmp)){ value = Number.parseFloat(value_tmp).toPrecision(3); } } const name = (colorings && colorings[trait] && colorings[trait].title) ? colorings[trait].title : trait; return isValueValid(value) ? item(name, value) : null; }; /** * A React component to display information about a tree tip in a modal-overlay style * @param {Object} props * @param {Object} props.tip tip node selected * @param {function} props.goAwayCallback * @param {object} props.colorings */ const TipClickedPanel = ({tip, goAwayCallback, colorings}) => { if (!tip) {return null;} const node = tip.n; return ( <div style={infoPanelStyles.modalContainer} onClick={() => goAwayCallback(tip)}> <div className={"panel"} style={infoPanelStyles.panel} onClick={(e) => stopProp(e)}> <StrainName>{node.name}</StrainName> <table> <tbody> <VaccineInfo node={node} /> <SampleDate node={node}/> <PublicationInfo node={node}/> {getTraitsToDisplay(node).map((trait) => ( <Trait node={node} trait={trait} colorings={colorings} key={trait}/> ))} <AccessionAndUrl node={node}/> </tbody> </table> <p style={infoPanelStyles.comment}> Click outside this box to go back to the tree </p> </div> </div> ); }; export default TipClickedPanel;