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
+
+
+
+ {{ section }}
+
+ onVirtualScroll(section, e)"
+ >
+
+
+
+
+
+ {{ idFormat }}
+
+ {{ field }}
+ |
+
+
+
+
+
+
+ {{ value ? value: "NA" }}
+ |
+
+
+
+
+
+
+ -
+
+ {{ value }}
+
+
+ {{ field }}
+
+ -
+ {{ subField }}: {{ subValue }}
+
+
+
+
+ {{ field ? field + ': ' + value : value }}
+
+
+
+
+
+
+
+
+
+
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:
+
+
+
+
+
+
+
+
+ `,
+ 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 }}
+
+
+ - File 1: {{ inputFile1 }}
+ - File 2: {{ inputFile2 }}
+
+
+
+ `,
+ 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
+
+
+
+
+
+
+
+
{{ section }}
+
+ - {{ field }}: {{ value }}
+
+
+
+
+
+
+ `,
+ 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
+
+
+
+ {{ section }}
+
+
+
+
+
+
+ ID
+
+ {{ idFormat }}
+
+ |
+
+ Number of Hits
+ |
+
+
+
+
+
+
+ {{ row.value }} |
+
+
+ {{ row.id }} |
+ {{ row.hits }} |
+
+
+ No data available |
+
+
+
+
+
+
+
+
+
+ `,
+ 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">
-
-
-
+
-
+
-
+
-