From 1b3ab46e89541b020449b2b2fa62c6273522c88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sunny=20=28SunJae=29=20Lee=20=EC=9D=B4=EC=84=A0=EC=9E=AC?= Date: Tue, 7 Jan 2025 16:39:42 +0900 Subject: [PATCH 1/7] Added comment --- frontend/ResultView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/ResultView.vue b/frontend/ResultView.vue index 397964d..baef36c 100644 --- a/frontend/ResultView.vue +++ b/frontend/ResultView.vue @@ -281,7 +281,7 @@ export default { activeTarget: null, alnBoxOffset: 0, selectedDatabases: 0, - isSankeyVisible: {}, // Track visibility for each entry.db + isSankeyVisible: {}, // Track visibility for each db's Sankey Diagram selectedDb: null, selectedTaxId: null, filteredHitsTaxIds: [], From 6c63ffedf31ff75474741eca78694fabed08fffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sunny=20=28SunJae=29=20Lee=20=EC=9D=B4=EC=84=A0=EC=9E=AC?= Date: Thu, 9 Jan 2025 12:11:53 +0900 Subject: [PATCH 2/7] Fixed algorithm for extracting lineage and creating links --- backend/alignment.go | 21 +++++- frontend/SankeyDiagram.vue | 141 +++++++++++-------------------------- 2 files changed, 61 insertions(+), 101 deletions(-) diff --git a/backend/alignment.go b/backend/alignment.go index 20c510a..50f3ab9 100644 --- a/backend/alignment.go +++ b/backend/alignment.go @@ -198,6 +198,7 @@ type TaxonomyReport struct { CladeReads int `json:"clade_reads"` TaxonReads int `json:"taxon_reads"` Rank string `json:"rank"` + Depth int `json:"depth"` TaxonID string `json:"taxon_id"` ScientificName string `json:"name"` } @@ -347,13 +348,26 @@ func ReadAlignments[T any, U interface{ ~uint32 | ~int64 }](id Id, entries []U, } func ReadTaxonomyReport(filePath string) ([]TaxonomyReport, error) { - file, err := os.Open(filePath) + file, err := os.Open(filePath) if err != nil { // Return an empty report for any error return []TaxonomyReport{}, nil } defer file.Close() + // Helper function to count leading spaces + countLeadingSpaces := func(s string) int { + count := 0 + for _, char := range s { + if char == ' ' { + count++ + } else { + break + } + } + return count + } + var reports []TaxonomyReport scanner := bufio.NewScanner(file) @@ -381,11 +395,16 @@ func ReadTaxonomyReport(filePath string) ([]TaxonomyReport, error) { continue } + // Calculate depth from leading spaces in ScientificName + indentCount := countLeadingSpaces(fields[5]) + depth := indentCount / 2 // Assuming 2 spaces per level of hierarchy + report := TaxonomyReport{ Proportion: proportion, CladeReads: cladeReads, TaxonReads: taxonReads, Rank: fields[3], + Depth: depth, TaxonID: fields[4], ScientificName: strings.TrimSpace(fields[5]), } diff --git a/frontend/SankeyDiagram.vue b/frontend/SankeyDiagram.vue index 5570ae7..ec4c84d 100644 --- a/frontend/SankeyDiagram.vue +++ b/frontend/SankeyDiagram.vue @@ -35,6 +35,7 @@ export default { // Data for graph rendering nonCladesRawData: null, // rawData with just clades filtered out allNodesByRank: {}, + sankeyRankColumns, rankOrderFull, // Imported from rankUtils rankOrder: [...sankeyRankColumns, "no rank"], colors: [ @@ -86,26 +87,6 @@ export default { } }, methods: { - // Function for processing/parsing data - processRawData(data) { - if (!this.rawData || !Array.isArray(this.rawData)) { - console.warn("rawData is not an array or is undefined", this.rawData); - return; - } - this.allNodesByRank = {}; // Reset the nodes by rank - - // Filter out clades from raw data - const nonClades = data.filter((entry) => this.rankOrderFull.includes(entry.rank) && entry.rank !== "clade"); - this.nonCladesRawData = nonClades; - - // Store nodes by rank from full data (for calculation of maxTaxaLimit) - nonClades.forEach((node) => { - if (!this.allNodesByRank[node.rank]) { - this.allNodesByRank[node.rank] = []; - } - this.allNodesByRank[node.rank].push(node); - }); - }, // Function for processing/parsing data parseData(data, isFullGraph = false) { const nodes = []; @@ -129,118 +110,78 @@ export default { name: d.name, rank: d.rank, trueRank: d.rank, + depth: d.depth, proportion: parseFloat(d.proportion), clade_reads: parseFloat(d.clade_reads), taxon_reads: d.taxon_reads, - lineage: [...currentLineage, { id: d.taxon_id, name: d.name, rank: d.rank }], // Copy current lineage + lineage: null, type: "", }; - if (d.rank !== "no rank" && !this.isUnclassifiedTaxa(d)) { - // Declare type as 'classified' - node.type = "classified"; - - // Add classified node to its corresponding rank collection - if (!nodesByRank[d.rank]) { - nodesByRank[d.rank] = []; - } - nodesByRank[d.rank].push(node); - - // Include all ranks for lineage tracking - if (node.rank !== "clade") { - let lastLineageNode = currentLineage[currentLineage.length - 1]; - if (lastLineageNode) { - let currentRank = rankHierarchyFull[node.rank] ?? Infinity; - let lastRank = rankHierarchyFull[lastLineageNode.rank] ?? Infinity; - - while (lastLineageNode && currentRank <= lastRank) { - const poppedNode = currentLineage.pop(); - lastLineageNode = currentLineage[currentLineage.length - 1]; - - if (!lastLineageNode) { - break; // Exit the loop if no more nodes in the lineage - } - - currentRank = rankHierarchyFull[node.rank] ?? Infinity; - lastRank = rankHierarchyFull[lastLineageNode.rank] ?? Infinity; - } + // Add node to its corresponding rank collection + if (!nodesByRank[d.rank]) { + nodesByRank[d.rank] = []; + } + nodesByRank[d.rank].push(node); + + // Store lineage for each node + let lastLineageNode = currentLineage[currentLineage.length - 1]; + if (lastLineageNode) { + let currentRank = node.depth; + let lastRank = lastLineageNode.depth; + + while (lastLineageNode && currentRank <= lastRank) { + const poppedNode = currentLineage.pop(); + + lastLineageNode = currentLineage[currentLineage.length - 1]; + if (!lastLineageNode) { + break; // Exit the loop if no more nodes in the lineage } - // Append current node to currentLineage array + store lineage data - currentLineage.push(node); - node.lineage = [...currentLineage]; + + lastRank = lastLineageNode.depth; // Update lastRank for the next iteration comparison } } + // Append current node to currentLineage array + store lineage data + currentLineage.push(node); + node.lineage = [...currentLineage]; }); - // Step 2: Filter top 10 nodes by clade_reads for each rank in rankOrder - // + Add filtered rank nodes & unclassified nodes to sankey diagram - this.rankOrder.forEach((rank) => { + // Step 2: Filter top 10 nodes by clade_reads for each rank + // + Add filtered rank nodes to sankey data + this.sankeyRankColumns.forEach((rank) => { if (nodesByRank[rank]) { - // Store all nodes - allNodes.push(...nodesByRank[rank]); - - // Sort nodes by clade_reads in descending order and select the top nodes based on slider value - const topNodes = nodesByRank[rank].sort((a, b) => b.clade_reads - a.clade_reads).slice(0, isFullGraph ? nodesByRank[rank].length : 10); // Don't apply taxaLimit when parsing fullGraphData + // Sort nodes by clade_reads in descending order and select the top nodes + const topNodes = nodesByRank[rank].sort((a, b) => b.clade_reads - a.clade_reads).slice(0, !isFullGraph ? nodesByRank[rank].length : 20); // Don't apply taxaLimit when parsing fullGraphData nodes.push(...topNodes); } }); - unclassifiedNodes.forEach((node) => { - // Store in all nodes - allNodes.push(node); - - // Add unclassified nodes to sankey - nodes.push(node); - }); - // Step 3: Create links based on filtered nodes' lineage nodes.forEach((node) => { // Find the previous node in the lineage that is in rankOrder const lineage = node.lineage; - let previousNode = null; - for (let i = lineage.length - 2; i >= 0; i--) { - // Start from the second last item - if (this.rankOrder.includes(lineage[i].rank) && nodes.includes(lineage[i])) { - previousNode = lineage[i]; - break; - } - } - - if (previousNode) { - links.push({ + let previousNode = lineage[lineage.length - 2]; + while (previousNode) { + const linkEntry = { + sourceName: previousNode.name, source: previousNode.id, + targetName: node.name, target: node.id, value: node.clade_reads, - }); - } - }); - - // Store links for all nodes - allNodes.forEach((node) => { - // Find the previous node in the lineage that is in rankOrder - const lineage = node.lineage; - let previousNode = null; + }; - for (let i = lineage.length - 2; i >= 0; i--) { - // Start from the second last item - if (this.rankOrder.includes(lineage[i].rank) && allNodes.includes(lineage[i])) { - previousNode = lineage[i]; + if (this.sankeyRankColumns.includes(previousNode.rank)) { + links.push(linkEntry); break; } - } - if (previousNode) { - allLinks.push({ - source: previousNode.id, - target: node.id, - value: node.clade_reads, - }); + previousNode = lineage[lineage.indexOf(previousNode) - 1]; } }); - return { nodes, links }; }, + // Main function for rendering Sankey createSankey(items) { const { nodes, links } = this.parseData(items); From 5c81049678c362f91f284fc420b40561251d6210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sunny=20=28SunJae=29=20Lee=20=EC=9D=B4=EC=84=A0=EC=9E=AC?= Date: Thu, 9 Jan 2025 12:19:29 +0900 Subject: [PATCH 3/7] Added back tax limit 10 --- backend/alignment.go | 3 ++- frontend/SankeyDiagram.vue | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/alignment.go b/backend/alignment.go index 50f3ab9..340fb6d 100644 --- a/backend/alignment.go +++ b/backend/alignment.go @@ -333,7 +333,8 @@ func ReadAlignments[T any, U interface{ ~uint32 | ~int64 }](id Id, entries []U, reader.Delete() // Read the taxonomy report - taxonomyReportPath := filepath.Join(base, "alis_" + db + "_report") + // taxonomyReportPath := filepath.Join(base, "alis_" + db + "_report") + taxonomyReportPath := filepath.Join(base, "alis_BFVD_report") taxonomyReport, _ := ReadTaxonomyReport(taxonomyReportPath) base := filepath.Base(name) diff --git a/frontend/SankeyDiagram.vue b/frontend/SankeyDiagram.vue index ec4c84d..bcad3e9 100644 --- a/frontend/SankeyDiagram.vue +++ b/frontend/SankeyDiagram.vue @@ -151,7 +151,7 @@ export default { this.sankeyRankColumns.forEach((rank) => { if (nodesByRank[rank]) { // Sort nodes by clade_reads in descending order and select the top nodes - const topNodes = nodesByRank[rank].sort((a, b) => b.clade_reads - a.clade_reads).slice(0, !isFullGraph ? nodesByRank[rank].length : 20); // Don't apply taxaLimit when parsing fullGraphData + const topNodes = nodesByRank[rank].sort((a, b) => b.clade_reads - a.clade_reads).slice(0, isFullGraph ? nodesByRank[rank].length : 10); // Don't apply taxaLimit when parsing fullGraphData nodes.push(...topNodes); } }); @@ -181,7 +181,7 @@ export default { }); return { nodes, links }; }, - + // Main function for rendering Sankey createSankey(items) { const { nodes, links } = this.parseData(items); From 27c4e71d08695ef897c3d7c867b6b02fc3eea320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sunny=20=28SunJae=29=20Lee=20=EC=9D=B4=EC=84=A0=EC=9E=AC?= Date: Thu, 9 Jan 2025 12:25:51 +0900 Subject: [PATCH 4/7] Fixed file path for taxonomy report --- backend/alignment.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/alignment.go b/backend/alignment.go index 340fb6d..50f3ab9 100644 --- a/backend/alignment.go +++ b/backend/alignment.go @@ -333,8 +333,7 @@ func ReadAlignments[T any, U interface{ ~uint32 | ~int64 }](id Id, entries []U, reader.Delete() // Read the taxonomy report - // taxonomyReportPath := filepath.Join(base, "alis_" + db + "_report") - taxonomyReportPath := filepath.Join(base, "alis_BFVD_report") + taxonomyReportPath := filepath.Join(base, "alis_" + db + "_report") taxonomyReport, _ := ReadTaxonomyReport(taxonomyReportPath) base := filepath.Base(name) From 6c8fab25b6877cd093d23383d618e928a7179658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sunny=20=28SunJae=29=20Lee=20=EC=9D=B4=EC=84=A0=EC=9E=AC?= Date: Fri, 10 Jan 2025 16:13:07 +0900 Subject: [PATCH 5/7] Fixed hits filtering algorithm --- frontend/ResultView.vue | 4 ++- frontend/SankeyDiagram.vue | 61 +++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/frontend/ResultView.vue b/frontend/ResultView.vue index baef36c..4339be9 100644 --- a/frontend/ResultView.vue +++ b/frontend/ResultView.vue @@ -406,9 +406,11 @@ export default { } // Filter each group to only include items with taxId in filteredAlignments - return alignments + const filteredAligments = alignments .map(group => group.filter(item => this.filteredHitsTaxIds.includes(Number(item.taxId)))) .filter(group => group.length > 0); + + return filteredAligments }, } }; diff --git a/frontend/SankeyDiagram.vue b/frontend/SankeyDiagram.vue index bcad3e9..120522b 100644 --- a/frontend/SankeyDiagram.vue +++ b/frontend/SankeyDiagram.vue @@ -95,14 +95,10 @@ export default { const links = []; const allLinks = []; - const rankHierarchyFull = this.rankOrderFull.reduce((acc, rank, index) => { - acc[rank] = index; - return acc; - }, {}); let currentLineage = []; const nodesByRank = {}; // Store nodes by rank for filtering top 10 - // Step 1: Create nodes and save lineage data for ALL NODES (excluding clade ranks) + // Step 1: Create nodes and save lineage data for ALL NODES data.forEach((d) => { let node = { id: d.taxon_id, @@ -110,7 +106,7 @@ export default { name: d.name, rank: d.rank, trueRank: d.rank, - depth: d.depth, + hierarchy: d.depth, proportion: parseFloat(d.proportion), clade_reads: parseFloat(d.clade_reads), taxon_reads: d.taxon_reads, @@ -119,26 +115,28 @@ export default { }; // Add node to its corresponding rank collection - if (!nodesByRank[d.rank]) { - nodesByRank[d.rank] = []; + if (this.sankeyRankColumns.includes(d.rank)) { + if (!nodesByRank[d.rank]) { + nodesByRank[d.rank] = []; + } + nodesByRank[d.rank].push(node); } - nodesByRank[d.rank].push(node); - + // Store lineage for each node let lastLineageNode = currentLineage[currentLineage.length - 1]; if (lastLineageNode) { - let currentRank = node.depth; - let lastRank = lastLineageNode.depth; + let currentRank = node.hierarchy; + let lastRank = lastLineageNode.hierarchy; while (lastLineageNode && currentRank <= lastRank) { const poppedNode = currentLineage.pop(); - + lastLineageNode = currentLineage[currentLineage.length - 1]; if (!lastLineageNode) { break; // Exit the loop if no more nodes in the lineage } - - lastRank = lastLineageNode.depth; // Update lastRank for the next iteration comparison + + lastRank = lastLineageNode.hierarchy; // Update lastRank for the next iteration comparison } } // Append current node to currentLineage array + store lineage data @@ -171,7 +169,7 @@ export default { value: node.clade_reads, }; - if (this.sankeyRankColumns.includes(previousNode.rank)) { + if (this.sankeyRankColumns.includes(previousNode.rank) && nodes.includes(previousNode)) { links.push(linkEntry); break; } @@ -185,7 +183,7 @@ export default { // Main function for rendering Sankey createSankey(items) { const { nodes, links } = this.parseData(items); - + // // Check if nodes and links are not empty if (!nodes.length || !links.length) { console.warn("No data to create Sankey diagram"); @@ -228,7 +226,6 @@ export default { nodes: nodes.map((d) => Object.assign({}, d)), links: links.map((d) => Object.assign({}, d)), }); - const color = d3.scaleOrdinal().range(this.colors); const unclassifiedLabelColor = "#696B7E"; @@ -325,7 +322,7 @@ export default { .attr("stroke", (d) => (d.target.type === "unclassified" ? unclassifiedLabelColor : color(d.source.color))) // Set link color to source node color with reduced opacity .attr("stroke-width", (d) => Math.max(1, d.width)); // .attr("clip-path", (d, i) => `url(#clip-path-${this.instanceId}-${i})`); - + // Create node group (node + labels) and add mouse events const nodeGroup = svg .append("g") @@ -360,7 +357,7 @@ export default {
Clade Reads
-
${d.value}
+
${d.clade_reads}
`); @@ -455,7 +452,7 @@ export default { .attr("dy", "0.35em") .attr("text-anchor", "middle") .style("font-size", "9px") - .text((d) => this.formatCladeReads(d.value)) + .text((d) => this.formatCladeReads(d.clade_reads)) .style("cursor", "pointer"); }, @@ -484,28 +481,24 @@ export default { return true; }, findChildren(rawData, selectedNode) { - const ids = []; + const filteredTaxIds = []; let startAdding = false; - - const rankIndex = sankeyRankColumns.reduce((acc, rank, index) => { - acc[rank] = index; - return acc; - }, {}); + const selectedNodeRank = selectedNode.hierarchy; for (let i = 0; i < rawData.length; i++) { - const d = rawData[i]; - if (d.taxon_id === selectedNode.taxon_id) { + const comparingNode = rawData[i]; + if (comparingNode.taxon_id === selectedNode.taxon_id) { // Start adding child nodes from here startAdding = true; continue; // Move to the next iteration to skip the current node } if (startAdding) { - const selectedNodeRank = rankIndex[selectedNode.trueRank] ?? -1; - const comparingNodeRank = rankIndex[d.rank] ?? Infinity; - if (comparingNodeRank > selectedNodeRank) { - ids.push(d.taxon_id); + const comparingNodeDepth = comparingNode.depth; + // console.log(selectedNode.name, selectedNode.hierarchy, "|", comparingNode.name, comparingNodeDepth); + if (comparingNodeDepth > selectedNodeRank) { + filteredTaxIds.push(comparingNode.taxon_id); } else { // Stop when we encounter a node at the same or higher rank break; @@ -513,7 +506,7 @@ export default { } } - return ids; + return filteredTaxIds; }, // Throttle function (used for improving performance during node hover) From 0ab1c21f078568672b447e68e614c525259e82d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sunny=20=28SunJae=29=20Lee=20=EC=9D=B4=EC=84=A0=EC=9E=AC?= Date: Fri, 10 Jan 2025 16:17:56 +0900 Subject: [PATCH 6/7] Added rank labels below sankey --- frontend/SankeyDiagram.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/SankeyDiagram.vue b/frontend/SankeyDiagram.vue index 120522b..525036b 100644 --- a/frontend/SankeyDiagram.vue +++ b/frontend/SankeyDiagram.vue @@ -203,10 +203,10 @@ export default { const marginRight = 70; const width = container.parentElement.clientWidth; // Dynamically get parent width - const height = 360 + marginBottom; // Fixed height for now + const height = 450; const svg = d3.select(container) - .attr("viewBox", `0 0 ${width} ${height}`) + .attr("viewBox", `0 0 ${width} ${height+marginBottom}`) .attr("width", "100%") .attr("height", height) .classed("hide", false); From 1eadb29a1bb77c5e9d77c03a77c9bffe5adf1c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sunny=20=28SunJae=29=20Lee=20=EC=9D=B4=EC=84=A0=EC=9E=AC?= Date: Mon, 13 Jan 2025 16:13:54 +0900 Subject: [PATCH 7/7] Added node for Unclassified Sequences below root node + cleaned up code --- frontend/SankeyDiagram.vue | 110 ++++++++++++++++++++++--------------- frontend/rankUtils.js | 77 +------------------------- 2 files changed, 69 insertions(+), 118 deletions(-) diff --git a/frontend/SankeyDiagram.vue b/frontend/SankeyDiagram.vue index 525036b..8e44103 100644 --- a/frontend/SankeyDiagram.vue +++ b/frontend/SankeyDiagram.vue @@ -7,7 +7,7 @@