import { virusInfoOrUndefinedF } from "/_js/functions.js"
import { availableVirusInfoF } from "/_js/functions.js"
import { availableToolsF } from "/_js/functions.js"
import { virusOfInterestF } from "/_js/functions.js"
import { encodeTool } from '/_js/functions.js'
FA = FileAttachment
// Load the toolbox
families = await FA("/families.json").json()
_toolbox = await FileAttachment("/toolbox/toolbox.json").json()
toolbox = _toolbox.map( tool => encodeTool(tool) )
// console.log(toolbox)
addEncodedTools = (virus) => {
return ({ ... virus, tools: toolbox.flatMap( tool => virus[tool.encoded] ? tool.id : []) })
}
// Load all Newick tree file and create a dictionary `family -> tree`
nwks = await Promise.all(
families.map(async (family) => {
let tree = await FA("/"+ family + "/tree.newick").text()
return ({ family: family, nwk: tree })
}))
nwksAsMap = Object.fromEntries(nwks.map( el => [el.family, el.nwk] ))
// console.log(nwks)
// Load annotations for viruses for each family
_annotations = await Promise.all(
families.map( async (family) => {
let xls = await FA("/"+ family + "/family.xlsx").xlsx()
let _annotations = xls.sheet(0, { headers: true, range: ":L" })
let _trimmed = _annotations.map( virus => {
let trimmed = {}
Object.keys(virus).forEach( key => {
trimmed[key] = virus[key].trim()
})
return trimmed
})
return _trimmed.map( virus => ({... virus, family: family }) )
}))
// console.log(_annotations)
// Add tools information, just the list of tools ids
annotations = _annotations
.flat()
.map( virus => addEncodedTools(virus) )
familyAnnotations = annotations
.filter(virus => virus.family == virusFamily)
function virusInfo(_family, _virus) {
const info =
familyAnnotations.filter(row => row.abbreviation == _virus && row.family == _family)
return info[0]
}
// Handle grouped viruses by extending the model with isGroup: false/true
addGrouped = (virus) => {
const isGroup = (typeof virus.group_abbreviation !== "undefined") && (virus.group_abbreviation !== "")
const virus_pointer = (isGroup) ? virus.group_abbreviation : virus.abbreviation
return (
{ ... virus,
isGroup: isGroup,
virusPointer: virus_pointer,
resVirusId: (isGroup) ? virus.group_abbreviation : virus.abbreviation,
resAbbreviation: (isGroup) ? virus.group_abbreviation : virus.abbreviation,
resVirusName: (isGroup) ? virus.group_virus_name : virus.virus_name
}
)
}
// Create a list of all viruses of interest in the Excel file
// Add group information early on
annotatedViruses =
familyAnnotations
.filter(virus => virus.virus_of_interest == "Yes")
.map(virus => addGrouped(virus))
// Create a sublist of viruses with toolbox
// Take into account groups by selecting unique entries
toolboxAnnotatedViruses =
annotatedViruses
.filter(virus => virus.availability_in_toolbox == "Yes")
.filter((value, index, self) => {
return self.findIndex(v => v.resAbbreviation === value.resAbbreviation) === index;
})
// Derive a virus object from the ID
// The ID can be either a virus_id (from the ictv tree) or a group_abbreviation
virusIdToVirus = (virus_id) => {
// lookup the virus_id in the full list of viruses
const fullList =
annotatedViruses
.filter(v => v.virus_id.replace(/^'+|'+$/g, '') == virus_id || v.abbreviation == virus_id)
//.filter(v => v.virus_id == virus_id || v.abbreviation == virus_id)
// lookup the abbreviation for the group
const groupList =
toolboxAnnotatedViruses
.filter(v => v.group_abbreviation == virus_id)
if (fullList.length > 0) {
return fullList[0]
}
if (groupList.length > 0) {
return groupList[0]
}
}
// Return all matches for a virus ID
virusIdToViruses = (virus_id) => {
// lookup the virus_id in the full list of viruses
const single_virus =
annotatedViruses
.filter(v => v.virus_id.replace(/^'+|'+$/g, '') == virus_id || v.abbreviation == virus_id)
[0]
if (single_virus.isGroup) {
return annotatedViruses
.filter(v => v.group_abbreviation == single_virus.group_abbreviation)
} else {
return [ single_virus ]
}
}
function renderVirusToolbox(family, virus_id) {
// First extract the virus object from the ID or abbreviation
// This takes into account the groups
const virus = virusIdToVirus(virus_id)
if (typeof virus.tools !== "undefined" && virus.tools.length > 0) {
return html`
${toolbox.filter(tool => virus.tools.includes(tool.id) ).map(tool =>
html`
<h2 id="${tool.id}-section">${tool.name}</h2>
<div class="container grid">
<div class="g-col-12 g-col-md-3 toolbox one-tool" style="text-align:center;">
<div id="toolbox-contents" class="tool-container">
<a href="/toolbox/index.html#${tool.id}-section">
<div class="tool-wrapper">
<div id="${tool.id}" class="tool">
<div class="tool-tooltip-text">${tool.name}</div>
${tool.icon.map( i =>
html`<img class="tool-icon" height="${(tool.icon.length > 1) ? 150/tool.icon.length : 100}%" src="${i}"/>`
)}
</div>
</div>
</a>
</div>
</div>
<div class="g-col-12 g-col-md-9">
<p>${tool.description}</p>
</div>
<div class="g-col-1" style="text-align:center;">
</div>
</div>
<div class="col-xs-12" style="height:20px;"></div>
`
)}
`
}
else {
return html`
<p> </p>
`
}
}
// Initialize a container to contain the containers for the tree and annotation window
html`
<!-- Show information on hovered virus, reuse the same class for convenience -->
<div class="phylo-container">
<!-- Left part -->
<div class="left">
<div id="info-container" class="text-center" style="visibility:hidden;">
<div id="name" class="g-col-md-12 bold">
name
</div>
<div id="toolbox" class="g-col-md-12">
no tools yet for this virus
</div>
</div>
</div>
<!-- Right part -->
<div id="annotation-container" class="right no-gutter">
<p> </p>
</div>
</div>
<div class="phylo-container">
<!-- Left part -->
<div class="left">
<div id="tree-container" class="tree-container">
<svg id="tree-svg" class="tree-svg"></svg>
</div>
<div id="legend-container" class="text-center mb-2">
<div class="grid">
<div class="g-col-md-12 small">
<span class="phylotree-node-text" style="font-size:15px !important;">HOVER</span> for more information /
<span class="phylotree-node-text-hl" style="font-size:15px !important;">BOLD NODES</span> are present in collection and can be selected
</div>
</div>
</div>
</div>
<!-- Right part -->
<div id="annotation-container" class="right no-gutter">
<div class="container grid small" align="center" style="--bs-gap: 1.0rem;font-weight:500;">
<div class="g-col-md-12 g-col-sm-12 card card-body tool-header opacity-100" id="virus">
Viral Toolbox
</div>
</div>
<div class="container">
<div class="toolbox">
<div id="toolbox-contents" class="tool-container">
</div>
</div>
</div>
</div>
</div>
`
virusInfoOrUndefined = virusInfoOrUndefinedF(familyAnnotations)
availableVirusInfo = availableVirusInfoF(familyAnnotations)
availableTools = availableToolsF(familyAnnotations, toolbox)
virusOfInterest = virusOfInterestF(familyAnnotations)
// Initialize the toolbox when the data is loaded
init_toolbox =
toolbox.map(tool =>
d3.select("#toolbox-contents").node().append(
html`
<a href=${get_link("toolbox", "#" + tool.id + "-section")} target="_top">
<div class="tool-wrapper">
<div id="${tool.id}" class="tool">
<div class="tool-tooltip-text">${tool.name}</div>
${tool.icon.map( i =>
html`<img class="tool-icon" height="${(tool.icon.length > 1) ? 150/tool.icon.length : 100}%" src="${i}"/>`
)}
</div>
</div>
</a>
`
)
)
// Reset the tools in the Toolbox
clearToolbox = () => {
d3.select("#virus").text("Viral Toolbox")
toolbox.map(tool => {
d3.select("#" + tool.id).attr("class", "tool")
})
}
// Update the tools in the toolbox based on the provided virus object
updateToolbox = (virus) => {
d3.select("#virus").text(virus.resVirusName)
toolbox
.filter(tool => virus.tools.includes(tool.id))
.map(tool =>
d3.select("#" + tool.id).attr("class", "tool tool-selected")
)
}
// Handle tabset depending on the number of tabs
rawTabs = d3.select("#vb-tabset").select("ul").selectAll("li").select("a").nodes()
nrTabs = rawTabs.length
function indexToTab(i, length) {
const pre = "tabset-" + (length + 1) + "-"
const post = "-tab"
return pre + (i+1) + post
}
tabsetAnchor = indexToTab(0, nrTabs)
virusIdToTabMap = new Map(
toolboxAnnotatedViruses
.map( (virus,i) => [ virus.resAbbreviation, indexToTab(i, nrTabs) ] )
)
tabToVirusIdMap = new Map(
Array.from(virusIdToTabMap, a => a.reverse())
)
// Hide virus information tabset
function hideTabset() {
const virusInfo = document.getElementById(tabsetAnchor).parentNode.parentNode.parentNode
virusInfo.style.display = "none"
}
hide = hideTabset()
// Instantiate tree
nwk = nwksAsMap[virusFamily]
tree = new phylotree.phylotree(nwk) // class Phylotree
// https://github.com/veg/phylotree.js/blob/master/src/main.js#L90
// import { update_tree } from "/_js/functions.js"
renderedTree = tree
// Transform the tree (translate and prune)
// based on the input Excel file for this virus family
.traverse_and_compute(node => {
const virus_id = node.data.name
const isLeaf = tree.isLeafNode(node)
if (isLeaf) {
const virus = virusIdToVirus(virus_id)
const isVirusOfInterest = (typeof virus !== "undefined") ? (virus.virus_of_interest == "Yes") : false
if (isVirusOfInterest) {
const currentName = virus_id
const newName = virus.abbreviation.trim()
node.data.name = newName
} else {
node.data.name = "remove_me"
tree.deleteANode(node)
}
}
})
.render({
'width': 600,
'height': 600,
'left-right-spacing': 'fit-to-size',
'top-bottom-spacing': 'fit-to-size',
'is-radial': true,
'selectable': false,
'collapsible': false,
'transitions' : false,
'show-menu': false,
'show-scale': false,
'align-tips': true,
'zoom': false,
'brush': false,
'draw-size-bubbles': false,
'maximum-per-node-spacing': 500,
'minimum-per-node-spacing': 100,
'maximum-per-level-spacing': 500,
'minimum-per-level-spacing': 100,
'container': "#tree-container"
})
.style_nodes( (element, data) => {
// the virus_id is how the virus is encoded in phylotree newick file
const virus_id = data.data.name
// At this stage in the process, every node should have information
const information = virusIdToVirus(virus_id) // virusInfoOrUndefined(virus_id)
const inToolbox = (typeof information !== "undefined") && (information.availability_in_toolbox == "Yes")
// Styling based on available information
const updtElement1 =
information
? element.select("text").attr("class", "phylotree-node-text")
: element.select("text").attr("class", "phylotree-node-text-inactive")
const updtElement = inToolbox ? element.select("text").attr("class", "phylotree-node-text-hl") : element
// Hover functionality - Virus name
if (information)
element
.on("mouseover", virusHoverIn(data, information, inToolbox))
.on("mouseout", virusHoverOut)
// Click functionality - Toolbox
inToolbox
? element.on('click', function(el) {
clearToolbox();
// Select the first tab in order to get to the root of the tabset
const virusInfo = document.getElementById(tabsetAnchor).parentNode.parentNode.parentNode
virusInfo.style.display = "block"
virusInfo.scrollIntoView(true)
// Now get the proper tab and open it
const thisVirusInfo = document.getElementById(virusIdToTabMap.get(information.virusPointer)) // either the virus_id or the group abbreviation
thisVirusInfo.click();
// Experiments
clearNodes()
highlightNodes(virus_id)
// Render the toolbox for this virus
updateToolbox(information);
})
: element.on('click', function() {
hideTabset()
clearToolbox()
})
})
.layout(true)
// Based on a virus ID (abbreviation), highlight the virus or the whole group it belongs to
highlightNodes = (virus_id) => {
const abbreviations = virusIdToViruses(virus_id).map(v => v.abbreviation)
d3.selectAll("g")
.filter(function (d) { return (typeof d.data !== "undefined") ? abbreviations.includes(d.data.name) : false})
.select("text").attr("class", "phylotree-node-text-selected")
}
// Initialize all viruses in the tree
clearNodes = () => {
const inLst = annotatedViruses.map(v => v.abbreviation)
const inToolboxLst = annotatedViruses.filter(v => v.availability_in_toolbox == "Yes").map(v => v.abbreviation)
// Initialize all text
d3.selectAll("g")
.filter(function (d) { return (typeof d.data !== "undefined") })
.select("text").attr("class", "phylotree-node-text")
// Update toolbox entries
d3.selectAll("g")
.filter(function (d) { return (typeof d.data !== "undefined") ? inToolboxLst.includes(d.data.name) : false})
.select("text").attr("class", "phylotree-node-text-hl")
}
// console.log(renderedTree)
showTree = renderedTree
.svg
.node()
// Show small virus information box on hover
virusHoverIn = (data, information, is_available) => () => {
// First, populate the content
const virusNameToShow =
(information.resVirusName)
? information.resVirusName
: (typeof information.virus_name !== "undefined")
? information.virus_name
: ""
d3.select("#name").text(virusNameToShow)
if (information.tools.length > 0)
d3.select("#toolbox").text("Viral tools are available, please select the virus for more information")
else
d3.select("#toolbox").text("No viral tools have been developed yet")
// ... then show the content
d3.select("#info-container").attr("style", "visibility:visible;")
}
virusHoverOut = () => {
d3.select("#info-container").attr("style", "visibility:hidden;")
}
// Clicking on the tabs feeds the info back to the toolbox info
feedbackToolbox = d3.selectAll(".nav-item")
.on('click', function(){
const _id = d3.select(this).select("a").attr('id')
const _virus_id = tabToVirusIdMap.get(_id)
// We should only update the toolbox if an actual virus is selected
// not if for instance the 'toolbox' tab is selected
if (typeof _virus_id !== "undefined") {
const _virus = virusIdToVirus(_virus_id)
clearToolbox();
updateToolbox(_virus)
clearNodes()
highlightNodes(_virus_id)
}
})
// Add SVG to the DOM
tmp = d3.select("#tree-container").node().appendChild(showTree)
// If the size of the SVG is smaller than the allocated 600px
// then resize it relatively to the fraction
// Allow for a bit of padding, hence the .95 factor.
newSizeF = (curSize) => {
if (curSize < 600) {
return 600 * (0.95 * 600 / curSize)
} else {
return curSize
}
}
newSize = newSizeF(renderedTree.size[0])
updateSize = renderedTree.set_size([newSize,newSize]).update()
The estimated incubation period is 3 to 6 days, and the median duration of illness can vary depending upon severity but is similar to other respiratory infections caused by viruses.
Secretions from coughing and sneezing.
Close personal contact, such as touching or shaking hands.
Touching objects or surfaces that have the viruses on them then touching the mouth, nose, or eyes.
The peak age of hospitalization for infants with hMPV occurs between 6–12 months of age, slightly older than the peak of RSV, which is around 2–3 months.
The clinical features and severity of HMPV are similar to those of RSV.
hMPV is associated with 5% to 40% of respiratory tract infections in hospitalized and outpatient children
No vaccines or antivirals are available.
Respiratory syncytial virus, or RSV, is a common respiratory virus that usually causes mild, cold-like symptoms.
Most people recover in a week or two, but RSV can be serious, especially for infants and older adults.
RSV is the most common cause of bronchiolitis (inflammation of the small airways in the lung) and pneumonia (infection of the lungs) in children younger than 1 year of age in the United States.
People infected with RSV are usually contagious for 3 to 8 days and may become contagious a day or two before they start showing signs of illness. However, some infants, and people with weakened immune systems, can continue to spread the virus even after they stop showing symptoms, for as long as 4 weeks.
RSV can survive for many hours on hard surfaces such as tables and crib rails. It typically lives on soft surfaces such as tissues and hands for shorter amounts of time.
(Reference: Graphs pertaining to RSV seasonality in Belgium have been sourced from Sciensano.)