From b8e057bde2f2fcc3909cd7eb965494888eaf2da6 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Wed, 5 Jun 2024 14:12:47 +1200 Subject: [PATCH 1/3] [minor] fix typo --- src/components/modal/LinkOutModalContents.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modal/LinkOutModalContents.jsx b/src/components/modal/LinkOutModalContents.jsx index 1746bfa80..e00a1b29f 100644 --- a/src/components/modal/LinkOutModalContents.jsx +++ b/src/components/modal/LinkOutModalContents.jsx @@ -80,7 +80,7 @@ const data = ({distanceMeasure, colorBy, mainTreeNumTips, tangle}) => { { name: 'taxonium.org', valid() { - // MicrobeTrace should work with all nextstrain URLs which support the {GET, accept JSON} route + // Taxonium should work with all nextstrain URLs which support the {GET, accept JSON} route // which should be ~all of them (except authn-required routes, which won't work cross-origin, // and which we don't attempt to detect here). Tanglegrams aren't supported. return !tangle; From 9f6e542beac72395b27d14954d45ef516fc9238b Mon Sep 17 00:00:00 2001 From: james hadfield Date: Wed, 5 Jun 2024 14:37:01 +1200 Subject: [PATCH 2/3] [link out modal] refactor The new design co-locates the data with the rendering, which is an anti-pattern but in this case it allows for each link-out to have much more flexibility in how it constructs the data. The co-location is necessary for the function (component) to access `useSelector`, with the inherent flexibility that provides. The trade-off is worth it in my opinion. --- src/components/modal/LinkOutModalContents.jsx | 221 +++++++++--------- 1 file changed, 115 insertions(+), 106 deletions(-) diff --git a/src/components/modal/LinkOutModalContents.jsx b/src/components/modal/LinkOutModalContents.jsx index e00a1b29f..ef753099e 100644 --- a/src/components/modal/LinkOutModalContents.jsx +++ b/src/components/modal/LinkOutModalContents.jsx @@ -73,103 +73,124 @@ const ButtonContainer = styled.div` align-items: center; ` -const data = ({distanceMeasure, colorBy, mainTreeNumTips, tangle}) => { - const pathname = window.location.pathname; - const origin = forceLinkOutHost || window.location.origin; - return [ - { - name: 'taxonium.org', - valid() { - // Taxonium should work with all nextstrain URLs which support the {GET, accept JSON} route - // which should be ~all of them (except authn-required routes, which won't work cross-origin, - // and which we don't attempt to detect here). Tanglegrams aren't supported. - return !tangle; - }, - description() { - return this.valid() ? ( - <> - Visualise this dataset in Taxonium (learn more). - - ) : ( - <> - {`The current dataset isn't viewable in taxonium ${tangle ? `as tanglegrams aren't supported` : ''}`} - - ) - }, - taxoniumColoring() { - if (isColorByGenotype(colorBy)) { - /* Taxonium syntax looks like 'color={"field":"genotype","pos":485,"gene":"M"}' - Note that taxonium (I think) does't backfill bases/residues for tips where there - are no observed mutations w.r.t. the root. - */ - const subfields = ['"genotype"']; // include quoting as taxonium uses - const colorInfo = decodeColorByGenotype(colorBy); - // Multiple mutations (positions) aren't allowed - if (!colorInfo || colorInfo.positions.length>1) return null; - // The (integer) position is not enclosed in double quotes - subfields.push(`"pos":${colorInfo.positions[0]}`); - // The gene value is optional, without it we use nucleotide ("nt" in taxonium syntax) - if (colorInfo.aa) subfields.push(`"gene":"${colorInfo.gene}"`); - // Note that this string will be encoded when converted to a URL - return `{"field":${subfields.join(',')}}`; - } - return `{"field":"meta_${colorBy}"}`; - }, - url() { - const baseUrl = 'https://taxonium.org'; - const queries = { - treeUrl: `${origin}${pathname}`, // no nextstrain queries - treeType: 'nextstrain', - ladderizeTree: 'false', // keep same orientation as Auspice - xType: distanceMeasure==='num_date' ? 'x_time' : 'x_dist', - } - const color = this.taxoniumColoring(); - if (color) queries.color = color; - - return `${baseUrl}?${Object.entries(queries).map(([k,v]) => `${k}=${encodeURIComponent(v)}`).join("&")}`; - } - }, - { - name: 'microbetrace.cdc.gov', - valid() { - // MicrobeTrace should work similarly to Taxonium (see above) - // but trees >500 tips are very slow to load - return !tangle; - }, - description() { - return this.valid() ? ( - <> - View this data in MicrobeTrace (learn more). - {mainTreeNumTips>500 && ( - - {` Note that trees with over 500 tips may have trouble loading (this one has ${mainTreeNumTips}).`} - - )} - - ) : ( - <> - {`The current dataset isn't viewable in MicrobeTrace ${tangle ? `as tanglegrams aren't supported` : ''}`} - - ) - }, - url() { - /** - * As of 2024-04-09, the 'origin' must be nextstrain.org or next.nextstrain.org - * for these links to work. This means (nextstrain.org) the links coming from heroku - * review apps will not work. - */ - const baseUrl = 'https://microbetrace.cdc.gov/MicrobeTrace'; - return `${baseUrl}?url=${encodeURIComponent(`${origin}${pathname}`)}` - }, - }, - ] +function clientDetails() { + return { + pathname: window.location.pathname, + origin: forceLinkOutHost || window.location.origin, + } } -export const LinkOutModalContents = () => { +function TaxoniumLinkOut() { + const displayName = 'taxonium.org'; + const {pathname, origin} = clientDetails(); const {distanceMeasure, colorBy, showTreeToo} = useSelector((state) => state.controls) + + // Taxonium should work with all nextstrain URLs which support the {GET, accept JSON} route + // which should be ~all of them (except authn-required routes, which won't work cross-origin, + // and which we don't attempt to detect here). Tanglegrams aren't supported. + if (showTreeToo) { + return ( + + {displayName} + + {`The current dataset isn't viewable in taxonium as tanglegrams aren't supported`} + + + ) + } + + return ( + + {displayName} + + Visualise this dataset in Taxonium (learn more). + + + ) + + function taxoniumColoring() { + if (isColorByGenotype(colorBy)) { + /* Taxonium syntax looks like 'color={"field":"genotype","pos":485,"gene":"M"}' + Note that taxonium (I think) does't backfill bases/residues for tips where there + are no observed mutations w.r.t. the root. + */ + const subfields = ['"genotype"']; // include quoting as taxonium uses + const colorInfo = decodeColorByGenotype(colorBy); + // Multiple mutations (positions) aren't allowed + if (!colorInfo || colorInfo.positions.length>1) return null; + // The (integer) position is not enclosed in double quotes + subfields.push(`"pos":${colorInfo.positions[0]}`); + // The gene value is optional, without it we use nucleotide ("nt" in taxonium syntax) + if (colorInfo.aa) subfields.push(`"gene":"${colorInfo.gene}"`); + // Note that this string will be encoded when converted to a URL + return `{"field":${subfields.join(',')}}`; + } + return `{"field":"meta_${colorBy}"}`; + } + + function url() { + const baseUrl = 'https://taxonium.org'; + const queries = { + treeUrl: `${origin}${pathname}`, // no nextstrain queries + treeType: 'nextstrain', + ladderizeTree: 'false', // keep same orientation as Auspice + xType: distanceMeasure==='num_date' ? 'x_time' : 'x_dist', + } + const color = taxoniumColoring(); + if (color) queries.color = color; + + return `${baseUrl}?${Object.entries(queries).map(([k,v]) => `${k}=${encodeURIComponent(v)}`).join("&")}`; + } +} + +function MicrobeTraceLinkOut() { + const displayName = 'microbetrace.cdc.gov'; + const {pathname, origin} = clientDetails(); + const {showTreeToo} = useSelector((state) => state.controls) const {mainTreeNumTips} = useSelector((state) => state.metadata); - const linkouts = data({distanceMeasure, colorBy, mainTreeNumTips, tangle: !!showTreeToo}); + // MicrobeTrace should work similarly to Taxonium (see above) + // but trees >500 tips are very slow to load (we don't prevent the display of such trees, + // however we do show a warning) + if (showTreeToo) { + return ( + + {displayName} + + {`The current dataset isn't viewable in MicrobeTrace as tanglegrams aren't supported`} + + + ) + } + + return ( + + {displayName} + + View this data in MicrobeTrace (learn more). + {mainTreeNumTips>500 && ( + + {` Note that trees with over 500 tips may have trouble loading (this one has ${mainTreeNumTips}).`} + + )} + + + ) + + function url() { + /** + * As of 2024-04-09, the 'origin' must be nextstrain.org or next.nextstrain.org + * for these links to work. This means (nextstrain.org) the links coming from heroku + * review apps will not work. + */ + const baseUrl = 'https://microbetrace.cdc.gov/MicrobeTrace'; + return `${baseUrl}?url=${encodeURIComponent(`${origin}${pathname}`)}` + } +} + + + +export const LinkOutModalContents = () => { return ( <>
@@ -187,20 +208,8 @@ export const LinkOutModalContents = () => {
- {linkouts.map((d) => ( - - {d.valid() ? ( - {d.name} - ) : ( - {d.name} - )} - {d.description()} - - ))} - - {linkouts.length===0 && ( -
{`The current data source and/or view settings aren't compatible with any platforms. Sorry!`}
- )} + + ); From e526494cda9c611269be3ffeb99282189a76863d Mon Sep 17 00:00:00 2001 From: james hadfield Date: Wed, 5 Jun 2024 15:16:45 +1200 Subject: [PATCH 3/3] [link out modal] add nextclade link-out Most discussion about this functionality has been happening within the nextclade repo, see and for a good summary. --- src/components/modal/LinkOutModalContents.jsx | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/components/modal/LinkOutModalContents.jsx b/src/components/modal/LinkOutModalContents.jsx index ef753099e..32a8a5da1 100644 --- a/src/components/modal/LinkOutModalContents.jsx +++ b/src/components/modal/LinkOutModalContents.jsx @@ -189,6 +189,58 @@ function MicrobeTraceLinkOut() { } +function NextcladeLinkOut() { + const displayName = 'nextclade'; + const {pathname, origin} = clientDetails(); + const {showTreeToo} = useSelector((state) => state.controls) + const {mainTreeNumTips, rootSequence} = useSelector((state) => state.metadata); + + // All datasets which have a root-sequence (either in-line or sidecar) can theoretically work as Nextclade + // datasets. See for more thorough discussion here. + // Excessively big trees may be problematic (as Nextclade was designed around smaller reference trees), but + // exactly what the threshold is isn't known. Here I use a rather ad-hoc tip-count threshold: + const largeTreeWarning = mainTreeNumTips > 4000; + + if ( + showTreeToo || // Tanglegrams won't work (surprise surprise!) + !rootSequence // Root sequence is required for Nextclade + ) { + return ( + + {displayName} + + {`The current tree isn't usable as a Nextclade dataset as ${ + showTreeToo ? + "tanglegrams aren't supported." : + "this dataset doesn't have a root-sequence (either within the main JSON or as a sidecar JSON)." + }`} + + + ) + } + + const url = `https://clades.nextstrain.org?dataset-json-url=${encodeURIComponent(`${origin}${pathname}`)}` + return ( + + {displayName} + + {`Use this tree as a nextclade reference dataset which allows you to add new sequences (via drag-and-drop) and see them placed on the tree. + Note that manually curated datasets may be better suited to your use case, see `} + clades.nextstrain.org + {` for all reference datasets or read the `} + + Nextclade Web documentation + + {` for more details.`} + {largeTreeWarning && ( + + {` Note that large trees such as this may not work in Nextclade!`} + + )} + + + ) +} export const LinkOutModalContents = () => { return ( @@ -208,6 +260,7 @@ export const LinkOutModalContents = () => {
+