diff --git a/pvaccompare/html_report/assets/css/styles.css b/pvaccompare/html_report/assets/css/styles.css new file mode 100644 index 0000000..32e2487 --- /dev/null +++ b/pvaccompare/html_report/assets/css/styles.css @@ -0,0 +1,45 @@ +.confirm-access-msg { + opacity: 0.75; +} + +.custom-border { + border: 1px solid darkgray; + width: 100%; +} + +.navbar-brand:hover { + color: darkgray; +} + +.navbar-nav .nav-item.active .nav-link { + color: white; +} + +.thead-sticky { + position: sticky; + top: 0; + background: black; + color: white; + z-index: 1; +} + +.thead-sticky th { + font-size: 1rem; +} + +.summary-notes { + color: red; +} + +.table-item { + background: rgb(50, 50, 50); + color: white; +} + +.table-item td { + font-size: 1rem !important; +} + +.identical-msg { + color: lime; +} \ No newline at end of file diff --git a/pvaccompare/html_report/favicon.ico b/pvaccompare/html_report/assets/favicon.ico similarity index 100% rename from pvaccompare/html_report/favicon.ico rename to pvaccompare/html_report/assets/favicon.ico diff --git a/pvaccompare/html_report/components/Differences.js b/pvaccompare/html_report/components/Differences.js new file mode 100644 index 0000000..5f1c73a --- /dev/null +++ b/pvaccompare/html_report/components/Differences.js @@ -0,0 +1,197 @@ +export default { + template: ` +
+

Differences

+
+
+ +
+
+
+
+

The files are identical

+
+ `, + props: [ + 'currentPageId', + 'currentComparison', + 'comparisonItems', + 'aggregatedData', + 'unaggregatedData', + 'inputYmlData', + 'jsonInputData', + 'referenceMatchesData', + 'hasUniqueVariants' + ], + + data() { + return { + visibleEntries: {}, + loadedSections: {}, + currentSectionIndex: {}, + loadBatchSize: 1000, + fields: ["ID", "File 1 Value", "File 2 Value", "File 1 Line", "File 2 Line"], + idFormat: "" + }; + }, + + computed: { + differences() { + return this.getDifferences(); + }, + + tableNeeded() { + if (this.currentComparison !== "Unavailable") { + return this.currentComparison.key !== 'inputYmlData' && this.currentComparison.key !== 'jsonInputData'; + } + return false; + } + }, + + watch: { + comparisonItems: function(items) { + if (items.length > 0) { + this.initializeData(); + } + }, + currentPageId(newPageId, oldPageId) { + if (newPageId !== oldPageId) { + this.initializeData(); + } + } + }, + + methods: { + getDifferences() { + if (this.currentComparison === "Unavailable") { + return {}; + } + + const key = this.currentComparison.key; + if (!this[key] || !this[key].differences) { + return {}; + } + + const differences = this[key].differences; + const formattedDifferences = {}; + + for (const section in differences) { + const sectionData = differences[section]; + + if (Array.isArray(sectionData)) { + formattedDifferences[section] = sectionData; + } else if (typeof sectionData === 'object') { + formattedDifferences[section] = {}; + + for (const nestedKey in sectionData) { + const nestedData = sectionData[nestedKey]; + + if (Array.isArray(nestedData)) { + formattedDifferences[section][nestedKey] = nestedData; + } else if (typeof nestedData === 'object') { + formattedDifferences[section][nestedKey] = nestedData; + } else { + formattedDifferences[section][nestedKey] = nestedData; + } + } + } + } + + return formattedDifferences; + }, + + initializeData() { + if (this.currentComparison === "Unavailable") { + return; + } + + const key = this.currentComparison.key; + if (this[key] && this[key].differences) { + this.idFormat = this[key].id_format; + this.visibleEntries = {}; + this.loadedSections = {}; + this.currentSectionIndex = {}; + + for (const section in this[key].differences) { + this.visibleEntries[section] = []; + this.loadedSections[section] = 0; + this.currentSectionIndex[section] = 1; + + this.loadSection(section); + } + } + }, + + loadSection(section) { + const key = this.currentComparison.key; + const sectionData = this[key]?.differences?.[section]; + if (!sectionData || this.currentSectionIndex[section] > sectionData.num_sections) { + return; + } + + const sectionKey = `section${this.currentSectionIndex[section]}`; + const newEntries = sectionData[sectionKey] || []; + this.visibleEntries[section] = [...this.visibleEntries[section], ...newEntries]; + this.currentSectionIndex[section]++; + }, + + onVirtualScroll(section, e) { + const bottomReached = e.index >= this.visibleEntries[section]?.length - 1; + if (bottomReached) { + this.loadSection(section); + } + }, + } +}; \ No newline at end of file diff --git a/pvaccompare/html_report/components/DirectorySelection.js b/pvaccompare/html_report/components/DirectorySelection.js new file mode 100644 index 0000000..61f3115 --- /dev/null +++ b/pvaccompare/html_report/components/DirectorySelection.js @@ -0,0 +1,158 @@ +export default { + template: ` +
+
+

pVACcompare

+
+
+

Select Results Folder

+

You will be asked to confirm directory access. This is a browser security feature.

+
+ +
+ +
+ + +
Selected Directory:
+
{{ selectedDirectory }}
+
+
+ + +
Files Found in Directory:
+
    +
  • {{ file.name }}
  • +
+
+
+
+ + +
+
+ `, + data() { + return { + filesInDirectory: [], + validFilesInDirectory: [], + selectedDirectory: null, + }; + }, + methods: { + onDirectorySelected(event) { + const files = event.target.files; + + if (files.length > 0) { + const path = files[0].webkitRelativePath.split('/')[0]; + this.selectedDirectory = path; + + const fileKeyMap = { + 'yml_input_data.json': 'inputYmlData', + 'unaggregated_data.json': 'unaggregatedData', + 'aggregated_data.json': 'aggregatedData', + 'reference_matches_data.json': 'referenceMatchesData', + 'json_input_data.json': 'jsonInputData', + }; + + Array.from(files).forEach((file, index) => { + const key = fileKeyMap[file.name] || null; + const reader = new FileReader(); + reader.onload = (e) => { + this.filesInDirectory.push({ + id: index, + name: file.name, + content: e.target.result, + key, + }); + }; + reader.readAsText(file); + }); + } else { + this.$q.notify({ + type: 'negative', + message: `Error: The selected directory is empty`, + }); + this.selectedDirectory = null; + this.filesInDirectory = []; + } + }, + + confirmSelection() { + if (!this.selectedDirectory) { + this.$q.notify({ + type: 'negative', + message: 'Error: Please select a directory', + }); + return; + } + + const validFileNames = [ + 'aggregated_data.json', + 'json_input_data.json', + 'reference_matches_data.json', + 'unaggregated_data.json', + 'yml_input_data.json' + ] + + this.validFilesInDirectory = this.filesInDirectory.filter(file => validFileNames.includes(file.name)); + if (this.validFilesInDirectory.length === 0) { + this.$q.notify({ + type: 'negative', + message: `Error: No valid files were found in the selected directory`, + }); + return; + } + const mhcClassI = []; + const mhcClassII = []; + + for (const file of this.validFilesInDirectory) { + if (file.content) { + const jsonData = JSON.parse(file.content); + if (jsonData.mhc_class === "1") { + if (mhcClassI.some(existingFile => existingFile.name === file.name)) { + this.$q.notify({ + type: 'negative', + message: `Error: Duplicate file detected in MHC Class I - ${file.name}`, + }); + return; + } + mhcClassI.push(file); + } else if (jsonData.mhc_class === "2") { + if (mhcClassII.some(existingFile => existingFile.name === file.name)) { + this.$q.notify({ + type: 'negative', + message: `Error: Duplicate file detected in MHC Class II - ${file.name}`, + }); + return; + } + mhcClassII.push(file); + } else { + console.warning(`File ${file.name} does not contain a valid mhc_class`); + } + } else { + console.warn(`File ${file.name} has no content.`); + } + } + + this.$emit('directory-selected', this.selectedDirectory, this.validFilesInDirectory, mhcClassI, mhcClassII); + this.$q.notify({ + type: 'positive', + message: 'Files loaded successfully', + }); + }, + }, +}; \ No newline at end of file diff --git a/pvaccompare/html_report/components/Header.js b/pvaccompare/html_report/components/Header.js new file mode 100644 index 0000000..1155c4b --- /dev/null +++ b/pvaccompare/html_report/components/Header.js @@ -0,0 +1,36 @@ +export default { + template: ` +
+

Selected Directory: {{ currentDirectory }}

+

{{ currentComparison.name }}

+
+ +
+
+ `, + props: [ + 'currentDirectory', + 'currentPageId', + 'currentComparison', + 'comparisonItems', + 'aggregatedData', + 'unaggregatedData', + 'inputYmlData', + 'jsonInputData', + 'referenceMatchesData' + ], + computed: { + inputFile1() { + const item = this.comparisonItems[this.currentPageId]; + return item && this[item.key] ? this[item.key].input_file1 : "Unavailable"; + }, + + inputFile2() { + const item = this.comparisonItems[this.currentPageId]; + return item && this[item.key] ? this[item.key].input_file2 : "Unavailable"; + } + } +}; \ No newline at end of file diff --git a/pvaccompare/html_report/components/Navbar.js b/pvaccompare/html_report/components/Navbar.js new file mode 100644 index 0000000..0627aab --- /dev/null +++ b/pvaccompare/html_report/components/Navbar.js @@ -0,0 +1,74 @@ +export default { + template: ` +
+ +
+ `, + props: [ + 'currentPageId', + 'currentClass', + 'comparisonItems', + 'hasMhcClassI', + 'hasMhcClassII' + ], + + data() { + return { + isDropdownOpen: false, + }; + }, + + methods: { + changeCurrentDirectory() { + this.$emit('change-directory'); + }, + + changeCurrentPage(id) { + this.$emit('change-page', id); + }, + + toggleDropdown() { + this.isDropdownOpen = !this.isDropdownOpen; + }, + + changeClass(classId) { + this.isDropdownOpen = false; + this.$emit('change-class', classId); + }, + + closeDropdownIfOutside(event) { + const dropdown = this.$refs.dropdown; + if (dropdown && !dropdown.contains(event.target) && this.isDropdownOpen) { + this.isDropdownOpen = false; + } + } + }, + mounted() { + document.addEventListener('click', this.closeDropdownIfOutside); + }, + + beforeUnmount() { + document.removeEventListener('click', this.closeDropdownIfOutside); + } +}; \ No newline at end of file diff --git a/pvaccompare/html_report/components/Summary.js b/pvaccompare/html_report/components/Summary.js new file mode 100644 index 0000000..ecd84c6 --- /dev/null +++ b/pvaccompare/html_report/components/Summary.js @@ -0,0 +1,85 @@ +export default { + template: ` +
+
+

Summary

+ +
+
+
+ +
+
+
+ `, + props: [ + 'currentPageId', + 'currentComparison', + 'comparisonItems', + 'aggregatedData', + 'unaggregatedData', + 'inputYmlData', + 'jsonInputData', + 'referenceMatchesData' + ], + data() { + return { + hide_summary: false, + summary: {} + }; + }, + + computed: { + displaySummary() { + return this.currentComparison.key !== 'inputYmlData' && this.currentComparison.key !== 'jsonInputData'; + } + }, + + watch: { + comparisonItems: function(items) { + if (items.length > 0) { + this.loadSummary(); + } + }, + currentPageId(newPageId, oldPageId) { + if (newPageId !== oldPageId) { + this.loadSummary(); + } + } + }, + + methods: { + toggleHideSummary() { + this.hide_summary = !this.hide_summary; + }, + + loadSummary() { + const key = this.currentComparison.key; + if (!this[key] || !this[key].summary) { + return null; + } + + const summary = this[key].summary; + if (key === 'inputYmlData') { + this.summary = {}; + } else if (key == 'jsonInputData') { + this.summary = {}; + } + this.summary = summary; + } + } +}; \ No newline at end of file diff --git a/pvaccompare/html_report/components/Variants.js b/pvaccompare/html_report/components/Variants.js new file mode 100644 index 0000000..f5b276b --- /dev/null +++ b/pvaccompare/html_report/components/Variants.js @@ -0,0 +1,118 @@ +export default { + template: ` +
+

Variants

+
+
+ +
+
+
+ `, + props: [ + 'currentPageId', + 'currentComparison', + 'comparisonItems', + 'aggregatedData', + 'unaggregatedData', + 'inputYmlData', + 'jsonInputData', + 'referenceMatchesData' + ], + data() { + return { + unique_variants: {}, + idFormat: "" + }; + }, + + computed: { + hasUniqueVariants() { + return Object.values(this.unique_variants).some( + (entries) => Array.isArray(entries) ? entries.length > 0 : Object.keys(entries).length > 0 + ); + } + }, + + watch: { + comparisonItems: function(items) { + if (items.length > 0) { + this.loadVariants(); + } + }, + currentPageId(newPageId, oldPageId) { + if (newPageId !== oldPageId) { + this.loadVariants(); + } + }, + hasUniqueVariants: function(value) { + this.$emit('has-unique-variants-changed', value); + } + }, + + methods: { + loadVariants() { + const key = this.currentComparison.key; + if (!this[key] || !this[key].variants) { + return null; + } + + this.idFormat = this[key].id_format; + const unique_variants = this[key].variants; + if (key === 'inputYmlData' || key === 'jsonInputData') { + this.unique_variants = {}; + } + this.unique_variants = unique_variants; + }, + + formattedEntries(entries) { + if (typeof entries === 'object' && !Array.isArray(entries)) { + return Object.entries(entries).map(([id, hits]) => ({ id, hits })); + } else if (Array.isArray(entries)) { + return entries.map((value, index) => ({ id: index + 1, value })); + } + return entries; + } + } +}; \ No newline at end of file diff --git a/pvaccompare/html_report/vue.global.js b/pvaccompare/html_report/libs/vue.global.js similarity index 100% rename from pvaccompare/html_report/vue.global.js rename to pvaccompare/html_report/libs/vue.global.js diff --git a/pvaccompare/html_report/main.html b/pvaccompare/html_report/main.html index e74e7d9..30b1847 100644 --- a/pvaccompare/html_report/main.html +++ b/pvaccompare/html_report/main.html @@ -3,9 +3,10 @@ - + + pVACcompare - Report @@ -47,7 +48,7 @@ :json-input-data="jsonInputData" :reference-matches-data="referenceMatchesData"> - - - + - + - + - - - - \ No newline at end of file + \ No newline at end of file