Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export Phyx view as CSV #238

Merged
merged 8 commits into from
Aug 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@phyloref/phyx": "^1.0.1",
"bootstrap": "^4.5.0",
"bootstrap-vue": "^2.15.0",
"csv-stringify": "^6.0.5",
"filesaver.js-npm": "^1.0.1",
"jquery": "^3.5.1",
"lodash": "^4.17.19",
Expand Down
6 changes: 6 additions & 0 deletions public/examples/brochu_2003.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
],
"phylorefs": [
{
"@id": "#Alligatoridae",
"label": "Alligatoridae",
"scientificNameAuthorship": {
"bibliographicCitation": "(Cuvier 1807)"
Expand Down Expand Up @@ -141,6 +142,7 @@
"externalSpecifiers": []
},
{
"@id": "#Alligatorinae",
"label": "Alligatorinae",
"scientificNameAuthorship": {
"bibliographicCitation": "(Kälin 1940)"
Expand Down Expand Up @@ -220,6 +222,7 @@
}
},
{
"@id": "#Caimaninae",
"label": "Caimaninae",
"scientificNameAuthorship": {
"bibliographicCitation": "(Norell 1988)"
Expand Down Expand Up @@ -298,6 +301,7 @@
}
},
{
"@id": "#Crocodyloidae",
"label": "Crocodyloidea",
"scientificNameAuthorship": {
"bibliographicCitation": "(Fitzinger 1826)"
Expand Down Expand Up @@ -350,6 +354,7 @@
}
},
{
"@id": "#Crocodylidae",
"label": "Crocodylidae",
"scientificNameAuthorship": {
"bibliographicCitation": "(Cuvier 1807)"
Expand Down Expand Up @@ -435,6 +440,7 @@
"externalSpecifiers": []
},
{
"@id": "#Diplocynodontinae",
"label": "Diplocynodontinae",
"scientificNameAuthorship": {
"bibliographicCitation": "(Brochu 1999)"
Expand Down
99 changes: 93 additions & 6 deletions src/components/phyx/PhyxView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@
>
Add phyloreference
</button>

<button
class="btn btn-secondary"
href="javascript:;"
@click="exportAsCSV()"
>
Export as CSV
</button>
</div>
</div>
</div>
Expand Down Expand Up @@ -266,8 +274,10 @@
* Display a summary of the entire Phyx file.
*/
import { mapState } from 'vuex';
import { has } from 'lodash';
import { PhylorefWrapper, PhylogenyWrapper, TaxonNameWrapper } from '@phyloref/phyx';
import { has, max, range } from 'lodash';
import { stringify } from 'csv-stringify';
import { saveAs } from 'filesaver.js-npm';
import {PhylorefWrapper, PhylogenyWrapper, TaxonNameWrapper, TaxonomicUnitWrapper} from '@phyloref/phyx';

export default {
name: 'PhyxView',
Expand All @@ -293,12 +303,12 @@ export default {
},
methods: {
getPhylogenyLabel(phylogeny) {
return new PhylogenyWrapper(phylogeny).label ||
`Phylogeny ${this.phylogenies.indexOf(phylogeny) + 1}`;
return phylogeny.label
|| `Phylogeny ${this.phylogenies.indexOf(phylogeny) + 1}`;
},
getPhylorefLabel(phyloref) {
return new PhylorefWrapper(phyloref).label ||
`Phyloref ${this.phylorefs.indexOf(phyloref) + 1}`;
return new PhylorefWrapper(phyloref).label
|| `Phyloref ${this.phylorefs.indexOf(phyloref) + 1}`;
},
hasReasoningResults(phyloref) {
if (!has(this.$store.state.resolution.reasoningResults, 'phylorefs')) return false;
Expand Down Expand Up @@ -360,6 +370,83 @@ export default {
this.$store.commit('deletePhylogeny', { phylogeny });
}
},
exportAsCSV() {
// Export the phyloref summary as CSV.

// Determine the maximum number of internal and external specifiers we will need to export.
const phylorefs = this.phylorefs;
const maxInternalSpecifiers = max(phylorefs.map(phyloref => phyloref.internalSpecifiers.length));
const maxExternalSpecifiers = max(phylorefs.map(phyloref => phyloref.externalSpecifiers.length));

// Create file header.
const header = [
'Phyloreference ID',
'Label',
'Type',
...range(0, maxInternalSpecifiers).map((_, i) => `Internal specifier ${i + 1}`),
...range(0, maxExternalSpecifiers).map((_, i) => `External specifier ${i + 1}`),
...this.phylogenies.flatMap((phylogeny) => {
const label = this.getPhylogenyLabel(phylogeny);
return [`${label} expected`, `${label} actual`];
}),
];

const rows = phylorefs.map((phyloref) => {
const wrappedPhyloref = new PhylorefWrapper(phyloref);

return [
this.$store.getters.getPhylorefId(phyloref),
wrappedPhyloref.label,
this.$store.getters.getPhylorefType(phyloref),
// Write out the internal specifier labels
...(wrappedPhyloref.internalSpecifiers.map(sp => new TaxonomicUnitWrapper(sp).label)),
// Write out blank cells for the remaining internal specifiers
...range(wrappedPhyloref.internalSpecifiers.length, maxInternalSpecifiers).map(() => ''),
// Write out the external specifier labels
...(wrappedPhyloref.externalSpecifiers.map(sp => new TaxonomicUnitWrapper(sp).label)),
// Write out blank cells for the remaining external specifiers
...range(wrappedPhyloref.externalSpecifiers.length, maxExternalSpecifiers).map(() => ''),
// Export phyloref expectation information.
...this.phylogenies.map((phylogeny) => {
const expectedNodeLabel = this.getPhylorefExpectedNodeLabel(phyloref, phylogeny);
if (!expectedNodeLabel) {
return '';
} else {
return expectedNodeLabel;
}
}),
// Export phyloref resolution information.
...this.phylogenies.map((phylogeny) => {
if (!this.hasReasoningResults(phyloref)) return 'Resolution not yet run';

const resolvedNodes = this.getNodeLabelsResolvedByPhyloref(phyloref, phylogeny);
return resolvedNodes.map(nl => (nl === '' ? 'an unlabeled node' : nl)).join('|');
}),
];
});

// Convert to CSV.
// console.log('Output:', [header, ...rows]);
stringify([
header,
...rows,
], (err, csv) => {
if (err) {
console.log('Error occurred while producing CSV:', err);
return;
}

const content = [csv];
// console.log('Content:', content);

// Save to local hard drive.
const filename = `${this.$store.getters.getDownloadFilenameForPhyx}.csv`;
const csvFile = new Blob(content, { type: 'text/csv;charset=utf-8' });
// Neither Numbers.app nor Excel can read the UTF-8 BOM correctly, so we explicitly
// turn it off.
saveAs(csvFile, filename, { autoBom: false });
});
},
},
};
</script>
26 changes: 1 addition & 25 deletions src/components/sidebar/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -251,31 +251,7 @@ export default {
},
computed: {
downloadFilenameForPhyx() {
// Return a filename to be used to name downloads of this Phyx document.

// The default download filename to use if no phylorefs are present.
const DEFAULT_DOWNLOAD_FILENAME = 'download';

if (!this.currentPhyx || !this.phylorefs) {
return DEFAULT_DOWNLOAD_FILENAME;
}

// Determine all phyloref labels in this document. Non-Latin characters will be replaced with '_' to avoid
// creating filenames using non-ASCII Unicode characters. As per the UI, unlabeled phylorefs will be referred
// to as 'Phyloref 1', 'Phyloref 2', and so on.
const phylorefLabels = this.phylorefs.map((p, index) => (has(p, 'label') ? p.label.replaceAll(/\W/g, '_') : `Phyloref_${index + 1}`));

// Construct a download filename depending on the number of phylorefs, which is in the form:
// - Phyloref_1
// - Phyloref_1_and_Phyloref_2
// - Phyloref_1_Phyloref_2_and_Phyloref_3
// - Phyloref_1_Phyloref_2_and_2_others
// - ...
if (phylorefLabels.length === 0) return DEFAULT_DOWNLOAD_FILENAME;
if (phylorefLabels.length === 1) return phylorefLabels[0];
if (phylorefLabels.length === 2) return `${phylorefLabels[0]}_and_${phylorefLabels[1]}`;
if (phylorefLabels.length === 3) return `${phylorefLabels[0]}_${phylorefLabels[1]}_and_${phylorefLabels[2]}`;
return `${phylorefLabels[0]}_${phylorefLabels[1]}_and_${phylorefLabels.length - 2}_others`;
return this.$store.getters.getDownloadFilenameForPhyx;
},
examplePHYXURLs() {
// Returns a list of example files to display in the "Examples" menu.
Expand Down
29 changes: 28 additions & 1 deletion src/store/modules/phyx.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,34 @@ export default {
},
getDefaultNomenCodeURI(state) {
return state.currentPhyx.defaultNomenclaturalCodeURI
|| TaxonNameWrapper.NAME_IN_UNKNOWN_CODE;
|| TaxonNameWrapper.UNKNOWN_CODE;
},
getDownloadFilenameForPhyx(state) {
// Return a filename to be used to name downloads of this Phyx document.

// The default download filename to use if no phylorefs are present.
const DEFAULT_DOWNLOAD_FILENAME = 'download';

if (!state.currentPhyx || !state.currentPhyx.phylorefs) {
return DEFAULT_DOWNLOAD_FILENAME;
}

// Determine all phyloref labels in this document. Non-Latin characters will be replaced with '_' to avoid
// creating filenames using non-ASCII Unicode characters. As per the UI, unlabeled phylorefs will be referred
// to as 'Phyloref 1', 'Phyloref 2', and so on.
const phylorefLabels = state.currentPhyx.phylorefs.map((p, index) => (has(p, 'label') ? p.label.replaceAll(/\W/g, '_') : `Phyloref_${index + 1}`));

// Construct a download filename depending on the number of phylorefs, which is in the form:
// - Phyloref_1
// - Phyloref_1_and_Phyloref_2
// - Phyloref_1_Phyloref_2_and_Phyloref_3
// - Phyloref_1_Phyloref_2_and_2_others
// - ...
if (phylorefLabels.length === 0) return DEFAULT_DOWNLOAD_FILENAME;
if (phylorefLabels.length === 1) return phylorefLabels[0];
if (phylorefLabels.length === 2) return `${phylorefLabels[0]}_and_${phylorefLabels[1]}`;
if (phylorefLabels.length === 3) return `${phylorefLabels[0]}_${phylorefLabels[1]}_and_${phylorefLabels[2]}`;
return `${phylorefLabels[0]}_${phylorefLabels[1]}_and_${phylorefLabels.length - 2}_others`;
},
},
mutations: {
Expand Down