Skip to content

Commit

Permalink
Merge branch 'master' into improve-documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
gaurav authored Jun 19, 2024
2 parents d9f349f + 6c0243f commit 17c38c2
Show file tree
Hide file tree
Showing 12 changed files with 487 additions and 231 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy-to-github-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Build Vue page in /docs
run: |
npm install
VUE_APP_VERSION=$GITHUB_REF_NAME npm run build
VITE_APP_VERSION=$GITHUB_REF_NAME npm run build
- name: Deploy /docs to branch gh-pages
uses: JamesIves/github-pages-deploy-action@v4
Expand Down
414 changes: 236 additions & 178 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"lodash": "^4.17.19",
"newick-js": "^1.2.1",
"pako": "^2.1.0",
"phylotree": "^1.4.0",
"phylotree": "^2.0.1",
"popper.js": "^1.16.1",
"vue": "^2.7.7",
"vue-cookies": "^1.8.1",
Expand Down
2 changes: 1 addition & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default {
AdvancedOptionsModal,
},
data: () => ({
version: process.env.VUE_APP_VERSION,
version: import.meta.env.VITE_APP_VERSION,
}),
computed: mapState({
display: state => state.ui.display,
Expand Down
12 changes: 8 additions & 4 deletions src/components/modals/AdvancedOptionsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,15 @@ export default {
// Exports the PHYX file as an OWL/JSON-LD file, which can be opened in
// Protege or converted into OWL/XML or other formats.
const wrapped = new PhyxWrapper(this.$store.state.phyx.currentPhyx);
const content = [JSON.stringify([wrapped.asJSONLD()], undefined, 4)];
try {
const content = [JSON.stringify([wrapped.asJSONLD()], undefined, 4)];
// Save to local hard drive.
const jsonldFile = new File(content, 'download.jsonld', { type: 'application/json;charset=utf-8' });
saveAs(jsonldFile);
// Save to local hard drive.
const jsonldFile = new File(content, `${this.$store.getters.getDownloadFilenameForPhyx}.jsonld`, { type: 'application/json;charset=utf-8' });
saveAs(jsonldFile, `${this.$store.getters.getDownloadFilenameForPhyx}.jsonld`);
} catch (err) {
alert(`Could not convert Phyx to JSON-LD: ${err}`);
}
},
},
};
Expand Down
22 changes: 22 additions & 0 deletions src/components/phylogeny/PhylogenyView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,13 @@ export default {
// Look for an unbalanced Newick string.
let parenLevels = 0;
let singleQuoteCount = 0;
let doubleQuoteCount = 0;
for (let x = 0; x < newickTrimmed.length; x += 1) {
if (newickTrimmed[x] === '(') parenLevels += 1;
if (newickTrimmed[x] === ')') parenLevels -= 1;
if (newickTrimmed[x] === '\'') singleQuoteCount += 1;
if (newickTrimmed[x] === '"') doubleQuoteCount += 1;
}
if (parenLevels !== 0) {
Expand All @@ -305,6 +309,24 @@ export default {
});
}
if (singleQuoteCount % 2 > 0) {
errors.push({
title: "Unbalanced single quotes (') in Newick string",
message:
`Every single quote should be closed by another single quote, but this Newick string has ` +
`${singleQuoteCount} single quotes. Try doubling the single quote to escape it.`,
});
}
if (doubleQuoteCount % 2 > 0) {
errors.push({
title: 'Unbalanced double quotes (") in Newick string',
message:
`Every double quote should be closed by another double quote, but this Newick string has ` +
`${doubleQuoteCount} double quotes. Try doubling the double quote to escape it.`,
});
}
// Finally, try parsing it with parseNewick and see if we get an error.
try {
parseNewick(newickTrimmed);
Expand Down
169 changes: 152 additions & 17 deletions src/components/phylogeny/Phylotree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@
<div v-else class="phylotreeContainer">
<div :id="'phylogeny' + phylogenyIndex" class="col-md-12 phylogeny" />
<ResizeObserver @notify="redrawTree" />
<button
type="button"
class="btn btn-primary"
@click="exportAsNexus()"
data-toggle="tooltip"
data-placement="bottom"
title="Download Nexus file with annotations of where phyloreferences resolve"
>
Download as Nexus
</button>
</div>
</div>
</template>
Expand All @@ -30,7 +40,8 @@ import { uniqueId, has } from "lodash";
import { phylotree, newickParser } from "phylotree";
import jQuery from "jquery";
import { PhylogenyWrapper, PhylorefWrapper } from "@phyloref/phyx";
import {addCustomMenu} from "phylotree/src/render/menus";
import { addCustomMenu } from "phylotree/src/render/menus";
import { saveAs } from "filesaver.js-npm";
/*
* Note that this requires the Phylotree Javascript to be loaded in the HTML
Expand Down Expand Up @@ -75,10 +86,14 @@ export default {
return this.phylogeny.newick || "()";
},
parsedNewick() {
return new PhylogenyWrapper(this.phylogeny).getParsedNewickWithIRIs(
this.$store.getters.getPhylogenyId(this.phylogeny),
newickParser
);
try {
return new PhylogenyWrapper(this.phylogeny).getParsedNewickWithIRIs(
this.$store.getters.getPhylogenyId(this.phylogeny),
newickParser
);
} catch {
return undefined;
}
},
newickErrors() {
// Check to see if the newick could actually be parsed.
Expand Down Expand Up @@ -130,6 +145,121 @@ export default {
this.redrawTree();
},
methods: {
exportAsNexus() {
// Store a list of annotations containing both double quotes and single quotes.
let annotations_containing_single_and_double_quotes = [];
// Export this phylogeny as a Nexus string in a .nex file for download.
const newickStr = this.tree.getNewick((node) => {
// Is the resolved node for this phyloref? If so, let's make an annotation.
if (
this.phyloref !== undefined &&
has(node, "data") &&
has(node.data, "@id")
) {
const annotations = [];
const data = node.data;
const convertToNexusAnnotationValue = (str) => {
// We really just need to wrap this in double-quotes, which means we need to filter out existing double
// quotes. We can do that by replacing them with a single quote. However, if there are already single
// quotes in the string, then this is a non-reversible action, so we should warn the user before doing
// that. If we need to do that, we add the annotations to annotations_containing_single_and_double_quotes,
// and ask the user for confirmation before export.
if (str.indexOf("'") >= 0 && str.indexOf('"') >= 0) {
annotations_containing_single_and_double_quotes.push(str);
}
// Replace double-quotes with a single quote.
return '"' + str.replaceAll('"', "'") + '"';
};
if (
this.$store.getters
.getResolvedNodesForPhylogeny(this.phylogeny, this.phyloref)
.includes(data["@id"])
) {
if (has(this.phyloref, "@id")) {
annotations.push(
"phyloref:actual=" +
convertToNexusAnnotationValue(this.phyloref["@id"])
);
}
if (has(this.phyloref, "label")) {
annotations.push(
"phyloref:actualLabel=" +
convertToNexusAnnotationValue(this.phyloref["label"])
);
}
// We don't know what to call this phyloref, but nevertheless we label it minimally.
if (!has(this.phyloref, "@id") && !has(this.phyloref, "label"))
annotations.push("phyloref:actual=");
}
if (
this.selectedNodeLabel &&
this.selectedNodeLabel.toLowerCase() === data.name.toLowerCase()
) {
if (has(this.phyloref, "@id")) {
annotations.push(
"phyloref:expected=" +
convertToNexusAnnotationValue(this.phyloref["@id"])
);
}
if (has(this.phyloref, "label")) {
annotations.push(
"phyloref:expectedLabel=" +
convertToNexusAnnotationValue(this.phyloref["label"])
);
}
// We don't know what to call this phyloref, but nevertheless we label it minimally.
if (!has(this.phyloref, "@id") && !has(this.phyloref, "label"))
annotations.push("phyloref:expected=");
}
console.log("Annotations: ", annotations);
if (annotations.length === 0) return undefined;
else return `[&${annotations.join(",")}]`;
}
return undefined;
});
// Create a Nexus file to store this `klados_tree` in.
const nexusStr = `#NEXUS\n\nBEGIN TREES;\n TREE klados_tree = ${newickStr}\nEND;\n`;
// If there are annotations containing both single and double quotes, we report them here and
// request confirmation from the user before exporting data that can't be exported.
if (annotations_containing_single_and_double_quotes.length > 0) {
const example_annotations = annotations_containing_single_and_double_quotes.slice(0, 3);
if (
!window.confirm(
`Some annotations (such as ${example_annotations.join(
"; "
)}) contain both single quotes and double quotes. To comply with NEXUS format, the double quotes need ` +
"to be converted to single quotes, but this will be non-reversible because the string already contains " +
"single quotes. Do you still want to export this NEXUS file?"
)
) {
// User doesn't want to do this export. Cancel this operation.
return;
}
}
// Write Nexus file to a location chosen by the user.
const filename = `${this.$store.getters.getDownloadFilenameForPhyx}.nex`;
// Save to local hard drive.
const newickFile = new File([nexusStr], filename, {
type: "text/plain;charset=utf-8",
});
// With charset=utf-8, saveAs defaults to adding a Unicode BOM.
// This will confuse downstream files, so we force it off here.
saveAs(newickFile, filename, { autoBom: false });
},
recurseNodes(node, func, nodeCount = 0, parentCount = undefined) {
// Recurse through PhyloTree nodes, executing function on each node.
// - node: The node to recurse from. The function will be called on node
Expand Down Expand Up @@ -245,20 +375,17 @@ export default {
const newName = window.prompt(
`Rename node named '${existingName}' to:`
);
// Apparently IE7 and IE8 will return the string 'undefined' if the user doesn't
// enter anything.
if (!newName || newName === "undefined") {
if (newName === null) {
// This means the user clicked "Cancel", so don't do anything.
} else if (!newName || newName === "undefined") {
// Apparently IE7 and IE8 will return the string 'undefined' if the user doesn't
// enter anything.
//
// Remove the current label.
node.name = "";
} else {
// Set the new label.
const escapedName = newName.replace("'", "''");
// If the name contains whitespace, escape it.
if (/\w/.test(escapedName)) {
node.name = "'" + escapedName + "'";
} else {
node.name = escapedName;
}
node.name = newName;
}
// Export the entire phylogeny as a Newick string, and store that
Expand Down Expand Up @@ -385,9 +512,17 @@ export default {
pinningNodeChildrenIRIs.add(source_id);
this.recurseNodes(source, (node) => {
pinningNodeChildrenIRIs.add(node["@id"]);
console.log("Found child", node["@id"], "for source", source_id);
console.log(
"Found child",
node["@id"],
"for source",
source_id
);
});
console.log("Set pinningNodeChildrenIRIs to ", pinningNodeChildrenIRIs);
console.log(
"Set pinningNodeChildrenIRIs to ",
pinningNodeChildrenIRIs
);
element.classed("descendant-of-pinning-node-branch", true);
} else if (pinningNodeChildrenIRIs.has(source_id)) {
Expand Down
10 changes: 7 additions & 3 deletions src/components/phyloref/PhylorefView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -648,14 +648,18 @@ export default {
},
getNodeLabels(phylogeny, nodeType) {
// Return a list of node labels in a particular phylogeny.
return new PhylogenyWrapper(phylogeny).getNodeLabels(nodeType).sort();
try {
return new PhylogenyWrapper(phylogeny).getNodeLabels(nodeType).sort();
} catch {
return [];
}
},
getExpectedNodeLabel(phylogeny) {
// Return the node label we expect this phyloref to resolve to on the
// specified phylogeny.
return this.$store.getters.getExpectedNodeLabel(
this.selectedPhyloref,
phylogeny,
this.selectedPhyloref,
phylogeny,
);
},
getSpecifierLabel(specifier) {
Expand Down
18 changes: 13 additions & 5 deletions src/components/phyx/PhyxView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -403,14 +403,22 @@ export default {
},
getPhylorefExpectedNodeLabel(phyloref, phylogeny) {
// Return a list of nodes that a phyloreference is expected to resolve to.
return this.$store.getters.getExpectedNodeLabel(phyloref, phylogeny);
return this.$store.getters.getExpectedNodeLabel(
phyloref,
phylogeny,
);
},
getNodesById(phylogeny, nodeId) {
// Return all node labels with this nodeId in this phylogeny.
const parsed = new PhylogenyWrapper(phylogeny).getParsedNewickWithIRIs(
this.$store.getters.getPhylogenyId(phylogeny),
newickParser
);
let parsed;
try {
parsed = new PhylogenyWrapper(phylogeny).getParsedNewickWithIRIs(
this.$store.getters.getPhylogenyId(phylogeny),
newickParser,
);
} catch {
return [];
}
function searchNode(node, results = []) {
if (has(node, "@id") && node["@id"] === nodeId) {
Expand Down
Loading

0 comments on commit 17c38c2

Please sign in to comment.