diff --git a/apps/agora/api/src/components/genes.ts b/apps/agora/api/src/components/genes.ts index c2cf8cdf5..c3ad2142c 100644 --- a/apps/agora/api/src/components/genes.ts +++ b/apps/agora/api/src/components/genes.ts @@ -1,25 +1,26 @@ // -------------------------------------------------------------------------- // // External // -------------------------------------------------------------------------- // -import { Request, Response, NextFunction } from 'express'; +import { NextFunction, Request, Response } from 'express'; // -------------------------------------------------------------------------- // // Internal // -------------------------------------------------------------------------- // -import { setHeaders, cache, altCache } from '../helpers'; -import { Gene, GeneCollection } from '../models'; import { - getRnaDifferentialExpression, - getProteomicsLFQ, - getProteomicsSRM, - getProteomicsTMT, - getMetabolomics, + getBioDomains, getExperimentalValidation, + getGeneLinks, + getMetabolomics, getNeuropathologicCorrelations, getOverallScores, - getGeneLinks, - getBioDomains, + getProteomicsLFQ, + getProteomicsSRM, + getProteomicsTMT, + getRnaDifferentialExpression, } from '.'; +import { altCache, cache, setHeaders } from '../helpers'; +import { Gene, GeneCollection } from '../models'; +import { getSimilarGenesNetwork } from './similar_genes_network'; // -------------------------------------------------------------------------- // // Functions @@ -38,12 +39,12 @@ export async function getAllGenes() { return result; } -export async function getGenes(ids?: string | string[]) { +export async function getGenes(ids?: string) { const genes: Gene[] = await getAllGenes(); if (ids) { - ids = typeof ids == 'string' ? ids.split(',') : ids; - return genes.filter((g: Gene) => ids?.includes(g.ensembl_gene_id)); + const ids_array = ids.split(','); + return genes.filter((g: Gene) => ids_array.includes(g.ensembl_gene_id)); } return genes; @@ -80,6 +81,7 @@ export async function getGene(ensg: string) { result.experimental_validation = await getExperimentalValidation(ensg); result.links = await getGeneLinks(ensg); result.bio_domains = await getBioDomains(ensg); + result.similar_genes_network = getSimilarGenesNetwork(result); } cache.set(cacheKey, result); @@ -162,7 +164,7 @@ export async function genesRoute(req: Request, res: Response, next: NextFunction } try { - const result = await getGenes(req.query.ids); + const result = await getGenes(req.query.ids); setHeaders(res); res.json(result); } catch (err) { diff --git a/apps/agora/api/src/components/similar_genes_network.ts b/apps/agora/api/src/components/similar_genes_network.ts new file mode 100644 index 000000000..56bc04829 --- /dev/null +++ b/apps/agora/api/src/components/similar_genes_network.ts @@ -0,0 +1,95 @@ +import { + Gene, + SimilarGenesNetwork, + SimilarGenesNetworkLink, + SimilarGenesNetworkNode, +} from '@sagebionetworks/agora/api-client-angular'; + +export function getSimilarGenesNetwork(gene: Gene): SimilarGenesNetwork { + const nodes: { [key: string]: SimilarGenesNetworkNode } = {}; + const links: { [key: string]: SimilarGenesNetworkLink } = {}; + const response: SimilarGenesNetwork = { + nodes: [], + links: [], + min: 0, + max: 0, + }; + + if (gene.links) { + Object.values(gene.links).forEach((link) => { + const a: string = link.geneA_ensembl_gene_id; + const b: string = link.geneB_ensembl_gene_id; + const key = a + b; + const rKey = b + a; + + // Check if a reverse link already exists + if (links[rKey] && !links[rKey].brain_regions.includes(link['brainRegion'])) { + links[rKey].brain_regions.push(link['brainRegion']); + return; + } + + if (!links[key]) { + links[key] = { + source: a, + target: b, + source_hgnc_symbol: link?.geneA_external_gene_name, + target_hgnc_symbol: link?.geneB_external_gene_name, + brain_regions: [link.brainRegion], + }; + } else if (!links[key].brain_regions.includes(link['brainRegion'])) { + links[key].brain_regions.push(link['brainRegion']); + } + }); + } + + response.links = Object.values(links).sort((a: any, b: any) => { + return a.brain_regions?.length - b.brain_regions?.length; + }); + + response.links.forEach((link: any) => { + link.brain_regions.sort(); + + ['source', 'target'].forEach((key: any) => { + if (!nodes[link[key]]) { + nodes[link[key]] = { + ensembl_gene_id: link[key], + hgnc_symbol: link[key + '_hgnc_symbol'], + brain_regions: link.brain_regions, + }; + } else { + link.brain_regions.forEach((brainRegion: any) => { + if (!nodes[link[key]].brain_regions.includes(brainRegion)) { + nodes[link[key]].brain_regions.push(brainRegion); + } + }); + } + }); + }); + + response.nodes = Object.values(nodes) + .sort((a: any, b: any) => { + return a.brain_regions?.length - b.brain_regions?.length; + }) + .reverse(); + + response.nodes.forEach((node: any, i: number) => { + node.brain_regions.sort(); + + if (node.brain_regions.length < response.min) { + response.min = node.brain_regions.length; + } + + if (node.brain_regions.length > response.max) { + response.max = node.brain_regions.length; + } + + // Insert current node to the beginning of the array + if (node.ensembl_gene_id === gene.ensembl_gene_id) { + const currentNode = node; + response.nodes.splice(i, 1); + response.nodes.unshift(currentNode); + } + }); + + return response; +} diff --git a/libs/agora/genes/src/lib/components/gene-network/gene-network.component.html b/libs/agora/genes/src/lib/components/gene-network/gene-network.component.html index 3ce0edb10..839806694 100644 --- a/libs/agora/genes/src/lib/components/gene-network/gene-network.component.html +++ b/libs/agora/genes/src/lib/components/gene-network/gene-network.component.html @@ -116,7 +116,7 @@

these genes are associated with AD.
- +
diff --git a/libs/agora/genes/src/lib/components/gene-network/gene-network.component.scss b/libs/agora/genes/src/lib/components/gene-network/gene-network.component.scss index fc940b4cd..9d5d08c13 100644 --- a/libs/agora/genes/src/lib/components/gene-network/gene-network.component.scss +++ b/libs/agora/genes/src/lib/components/gene-network/gene-network.component.scss @@ -1,8 +1,8 @@ /* stylelint-disable no-descending-specificity */ @use 'sass:map'; -@import 'libs/agora/styles/src/lib/variables'; -@import 'libs/agora/styles/src/lib/mixins'; +@use 'libs/agora/styles/src/lib/variables'; +@use 'libs/agora/styles/src/lib/mixins'; .gene-network { min-height: 600px; @@ -37,7 +37,7 @@ display: block; width: 15px; height: 15px; - background-color: map.get($gene-network-colors, 'selected'); + background-color: map.get(variables.$gene-network-colors, 'selected'); border-radius: 50%; cursor: pointer; } @@ -54,7 +54,7 @@ height: 4px; top: 6px; left: 0; - background-color: map.get($gene-network-colors, 'selected'); + background-color: map.get(variables.$gene-network-colors, 'selected'); } } @@ -82,7 +82,7 @@ svg { margin-right: 10px; - color: map.get($gene-network-colors, 'main'); + color: map.get(variables.$gene-network-colors, 'main'); } &:not(:first-child) { @@ -97,19 +97,19 @@ } &:nth-child(2)::before { - background-color: map.get($gene-network-colors, 'selected'); + background-color: map.get(variables.$gene-network-colors, 'selected'); } &:nth-child(3)::before { - background-color: map.get($gene-network-colors, '2-3'); + background-color: map.get(variables.$gene-network-colors, '2-3'); } &:nth-child(4)::before { - background-color: map.get($gene-network-colors, '4-5'); + background-color: map.get(variables.$gene-network-colors, '4-5'); } &:nth-child(5)::before { - background-color: map.get($gene-network-colors, '>6'); + background-color: map.get(variables.$gene-network-colors, '>6'); } } } @@ -157,7 +157,7 @@ .gene-network-selected-similar-list { a { - @include link; + @include mixins.link; display: inline-block; font-weight: 700; @@ -199,50 +199,79 @@ .network-chart { svg { .network-chart-link { - stroke: map.get($gene-network-colors, '>6'); + stroke: map.get(variables.$gene-network-colors, '>6'); &.edges-0, &.edges-1 { - stroke: map.get($gene-network-colors, 'default'); + stroke: map.get(variables.$gene-network-colors, 'default'); } &.edges-2, &.edges-3 { - stroke: map.get($gene-network-colors, '2-3'); + stroke: map.get(variables.$gene-network-colors, '2-3'); } &.edges-4, &.edges-5 { - stroke: map.get($gene-network-colors, '4-5'); + stroke: map.get(variables.$gene-network-colors, '4-5'); } } .network-chart-node { - fill: map.get($gene-network-colors, '>6'); + fill: map.get(variables.$gene-network-colors, '>6'); cursor: pointer; &.edges-0, &.edges-1 { - fill: map.get($gene-network-colors, 'default'); + fill: map.get(variables.$gene-network-colors, 'default'); } &.edges-2, &.edges-3 { - fill: map.get($gene-network-colors, '2-3'); + fill: map.get(variables.$gene-network-colors, '2-3'); } &.edges-4, &.edges-5 { - fill: map.get($gene-network-colors, '4-5'); + fill: map.get(variables.$gene-network-colors, '4-5'); } &.main { - fill: map.get($gene-network-colors, 'main'); + fill: map.get(variables.$gene-network-colors, 'main'); } &.selected { - fill: map.get($gene-network-colors, 'selected'); + fill: map.get(variables.$gene-network-colors, 'selected'); } } } } + +.row { + display: flex; + flex-wrap: nowrap; + max-width: 100%; + + .col-lg-8, + .col-lg-4 { + padding: 0 12px; + flex: 0 0 auto; + } + + .col-lg-8 { + width: 66.67%; + } + + .col-lg-4 { + width: 33.33%; + } + + @media (max-width: variables.$lg-breakpoint) { + flex-wrap: wrap; + + .col-lg-8, + .col-lg-4 { + flex: 1 1 100%; + } + } +} diff --git a/libs/agora/genes/src/lib/components/gene-network/gene-network.component.ts b/libs/agora/genes/src/lib/components/gene-network/gene-network.component.ts index 9aab3ed6b..436808f88 100644 --- a/libs/agora/genes/src/lib/components/gene-network/gene-network.component.ts +++ b/libs/agora/genes/src/lib/components/gene-network/gene-network.component.ts @@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common'; import { Component, Input, ViewEncapsulation, inject } from '@angular/core'; import { Router } from '@angular/router'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { Gene, GenesService, @@ -18,7 +20,7 @@ import { TooltipModule } from 'primeng/tooltip'; @Component({ selector: 'agora-gene-network', - imports: [CommonModule, NetworkChartComponent, TooltipModule], + imports: [CommonModule, FontAwesomeModule, NetworkChartComponent, TooltipModule], providers: [GenesService], templateUrl: './gene-network.component.html', styleUrls: ['./gene-network.component.scss'], @@ -44,6 +46,8 @@ export class GeneNetworkComponent { filters: number[] = []; selectedFilter = 1; + faAngleRight = faAngleRight; + init() { if (!this._gene?.similar_genes_network?.nodes?.length) { this.data = undefined; @@ -87,7 +91,7 @@ export class GeneNetworkComponent { } onNodeClick(node: NetworkChartNode) { - this.geneService.getGene(node.id).subscribe((gene: any) => { + this.geneService.getGene(node.id).subscribe((gene) => { this.selectedGene = gene; }); } diff --git a/libs/agora/genes/src/lib/components/gene-similar/gene-similar.component.ts b/libs/agora/genes/src/lib/components/gene-similar/gene-similar.component.ts index 4e30c9044..77c539ee8 100644 --- a/libs/agora/genes/src/lib/components/gene-similar/gene-similar.component.ts +++ b/libs/agora/genes/src/lib/components/gene-similar/gene-similar.component.ts @@ -1,9 +1,9 @@ import { Component, inject, OnInit } from '@angular/core'; -import { Router, ActivatedRoute, ParamMap, RouterLink } from '@angular/router'; -import { Gene } from '@sagebionetworks/agora/api-client-angular'; -import { GeneService, HelperService } from '@sagebionetworks/agora/services'; -import { GeneTableComponent } from '../gene-table/gene-table.component'; +import { ActivatedRoute, ParamMap, Router, RouterLink } from '@angular/router'; +import { Gene, GenesService } from '@sagebionetworks/agora/api-client-angular'; +import { HelperService } from '@sagebionetworks/agora/services'; import { ModalLinkComponent, SvgIconComponent } from '@sagebionetworks/agora/shared'; +import { GeneTableComponent } from '../gene-table/gene-table.component'; interface TableColumn { field: string; @@ -15,14 +15,14 @@ interface TableColumn { selector: 'agora-gene-similar', standalone: true, imports: [ModalLinkComponent, GeneTableComponent, RouterLink, SvgIconComponent], - providers: [GeneService], + providers: [GenesService], templateUrl: './gene-similar.component.html', styleUrls: ['./gene-similar.component.scss'], }) export class GeneSimilarComponent implements OnInit { route = inject(ActivatedRoute); router = inject(Router); - geneService = inject(GeneService); + geneService = inject(GenesService); helperService = inject(HelperService); gene: Gene = {} as Gene; @@ -79,12 +79,12 @@ export class GeneSimilarComponent implements OnInit { return; } - const ids: any = []; - this.gene.similar_genes_network.nodes.forEach((obj: any) => { - ids.push(obj.ensembl_gene_id); + const ids_array: string[] = []; + this.gene.similar_genes_network.nodes.forEach((obj) => { + ids_array.push(obj.ensembl_gene_id); }); - this.geneService.getGenes(ids).subscribe((genes) => { + this.geneService.getGenes(ids_array.join(',')).subscribe((genes) => { genes.forEach((de: Gene) => { // Populate display fields & set default values de.is_any_rna_changed_in_ad_brain_display_value = de.rna_brain_change_studied diff --git a/libs/agora/services/src/index.ts b/libs/agora/services/src/index.ts index 0049c1808..32d0a79ff 100644 --- a/libs/agora/services/src/index.ts +++ b/libs/agora/services/src/index.ts @@ -1,5 +1,4 @@ export * from './lib/error.service'; -export * from './lib/gene.service'; export * from './lib/github.service'; export * from './lib/helper.service'; export * from './lib/http-error-interceptor'; diff --git a/libs/agora/services/src/lib/gene.service.ts b/libs/agora/services/src/lib/gene.service.ts deleted file mode 100644 index 71965ad68..000000000 --- a/libs/agora/services/src/lib/gene.service.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { inject, Injectable } from '@angular/core'; -import { - BioDomain, - BioDomainInfo, - BioDomainsService, - Distribution, - DistributionService, - Gene, - GenesService, - SimilarGenesNetwork, - SimilarGenesNetworkLink, - SimilarGenesNetworkNode, -} from '@sagebionetworks/agora/api-client-angular'; -import { of, Observable } from 'rxjs'; -import { map, tap, share, finalize } from 'rxjs/operators'; - -@Injectable() -export class GeneService { - genes: { [key: string]: Gene } = {}; - distribution: Distribution | undefined = undefined; - distributionObservable: Observable | undefined; - comparisonData: any = {}; - - bioDomains: { [key: string]: BioDomain[] } = {}; - allBioDomains: BioDomainInfo[] = []; - - genesService = inject(GenesService); - bioDomainsService = inject(BioDomainsService); - distributionService = inject(DistributionService); - - getGene(id: string): Observable { - if (this.genes[id]) { - return of(this.genes[id]); - } - - return this.genesService.getGene(id).pipe( - map((gene: Gene | null) => { - if (!gene) return null; - gene.similar_genes_network = this.getSimilarGenesNetwork(gene); - return (this.genes[id] = gene); - }), - ); - } - - // ------------------------------------------------------------------------ // - - getGenes(ids: string | string[]): Observable { - if (typeof ids === 'object') { - ids = ids.join(','); - } - - return this.genesService.getGenes(ids); - } - - getStatisticalModels(gene: Gene) { - const models: string[] = []; - - gene.rna_differential_expression?.forEach((item: any) => { - if (!models.includes(item.model)) { - models.push(item.model); - } - }); - - return models; - } - - getSimilarGenesNetwork(gene: Gene): SimilarGenesNetwork { - const nodes: { [key: string]: SimilarGenesNetworkNode } = {}; - const links: { [key: string]: SimilarGenesNetworkLink } = {}; - const response: SimilarGenesNetwork = { - nodes: [], - links: [], - min: 0, - max: 0, - }; - - if (gene.links) { - Object.values(gene.links).forEach((link) => { - const a: string = link.geneA_ensembl_gene_id; - const b: string = link.geneB_ensembl_gene_id; - const key = a + b; - const rKey = b + a; - - // Check if a reverse link already exists - if (links[rKey] && !links[rKey].brain_regions.includes(link['brainRegion'])) { - links[rKey].brain_regions.push(link['brainRegion']); - return; - } - - if (!links[key]) { - links[key] = { - source: a, - target: b, - source_hgnc_symbol: link?.geneA_external_gene_name, - target_hgnc_symbol: link?.geneB_external_gene_name, - brain_regions: [link.brainRegion], - }; - } else if (!links[key].brain_regions.includes(link['brainRegion'])) { - links[key].brain_regions.push(link['brainRegion']); - } - }); - } - - response.links = Object.values(links).sort((a: any, b: any) => { - return a.brain_regions?.length - b.brain_regions?.length; - }); - - response.links.forEach((link: any) => { - link.brain_regions.sort(); - - ['source', 'target'].forEach((key: any) => { - if (!nodes[link[key]]) { - nodes[link[key]] = { - ensembl_gene_id: link[key], - hgnc_symbol: link[key + '_hgnc_symbol'], - brain_regions: link.brain_regions, - }; - } else { - link.brain_regions.forEach((brainRegion: any) => { - if (!nodes[link[key]].brain_regions.includes(brainRegion)) { - nodes[link[key]].brain_regions.push(brainRegion); - } - }); - } - }); - }); - - response.nodes = Object.values(nodes) - .sort((a: any, b: any) => { - return a.brain_regions?.length - b.brain_regions?.length; - }) - .reverse(); - - response.nodes.forEach((node: any, i: number) => { - node.brain_regions.sort(); - - if (node.brain_regions.length < response.min) { - response.min = node.brain_regions.length; - } - - if (node.brain_regions.length > response.max) { - response.max = node.brain_regions.length; - } - - // Insert current node to the beginning of the array - if (node.ensembl_gene_id === gene.ensembl_gene_id) { - const currentNode = node; - response.nodes.splice(i, 1); - response.nodes.unshift(currentNode); - } - }); - - return response; - } - - // ------------------------------------------------------------------------ // - - getBiodomains(ensg: string): Observable { - if (this.bioDomains[ensg]) { - return of(this.bioDomains[ensg]); - } - - return this.bioDomainsService.getBioDomain(ensg).pipe( - map((data: BioDomain[]) => { - return (this.bioDomains[ensg] = data); - }), - ); - } - - // ------------------------------------------------------------------------ // - - getAllBiodomains(): Observable { - if (this.allBioDomains.length > 0) { - return of(this.allBioDomains); - } - - return this.bioDomainsService.listBioDomains(); - } - - // ------------------------------------------------------------------------ // - - getDistribution(): Observable { - if (this.distribution) { - return of(this.distribution); - } else if (this.distributionObservable) { - return this.distributionObservable; - } else { - this.distributionObservable = this.distributionService.getDistribution().pipe( - tap((data: any) => (this.distribution = data)), - share(), - finalize(() => (this.distributionObservable = undefined)), - ); - return this.distributionObservable; - } - } - - // ------------------------------------------------------------------------ // - - getComparisonGenes( - category: 'RNA - Differential Expression' | 'Protein - Differential Expression', - subCategory: string, - ) { - const key = category + subCategory; - if (this.comparisonData[key]) { - return of(this.comparisonData[key]); - } else { - return this.genesService.getComparisonGenes(category, subCategory).pipe( - tap((data) => { - this.comparisonData[key] = data; - }), - ); - } - } -} diff --git a/libs/agora/styles/src/lib/_variables.scss b/libs/agora/styles/src/lib/_variables.scss index d4c0310d1..7bc498967 100644 --- a/libs/agora/styles/src/lib/_variables.scss +++ b/libs/agora/styles/src/lib/_variables.scss @@ -1,25 +1,30 @@ /* stylelint-disable no-duplicate-selectors */ @use 'sass:map'; +$sm-breakpoint: 600px; +$md-breakpoint: 768px; +$lg-breakpoint: 992px; +$xl-breakpoint: 1200px; + // -------------------------------------------------------------------------- // // Legacy // -------------------------------------------------------------------------- // $breakpoints: ( 'ex-small': ( - max-width: 600px, + max-width: $sm-breakpoint, ), 'small': ( - min-width: 600px, + min-width: $sm-breakpoint, ), 'medium': ( - min-width: 768px, + min-width: $md-breakpoint, ), 'large': ( - min-width: 992px, + min-width: $lg-breakpoint, ), 'ex-large': ( - min-width: 1200px, + min-width: $xl-breakpoint, ), ); diff --git a/libs/agora/testing/src/lib/mocks/api-service-stub.ts b/libs/agora/testing/src/lib/mocks/api-service-stub.ts index 56f465227..80d033e5e 100644 --- a/libs/agora/testing/src/lib/mocks/api-service-stub.ts +++ b/libs/agora/testing/src/lib/mocks/api-service-stub.ts @@ -3,8 +3,8 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; -import { Gene, GenesResponse, GCTGeneResponse, Distribution } from '@sagebionetworks/agora/models'; -import { geneMock1, geneMock2, gctGeneMock1, nominatedGeneMock1, teamsResponseMock } from './'; +import { Distribution, GCTGeneResponse, Gene, GenesResponse } from '@sagebionetworks/agora/models'; +import { gctGeneMock1, geneMock1, geneMock2, nominatedGeneMock1, teamsResponseMock } from './'; import { TeamsList } from '@sagebionetworks/agora/api-client-angular'; @@ -14,7 +14,7 @@ export class ApiServiceStub { return of(geneMock1); } - getGenes(ids: string | string[]): Observable { + getGenes(ids: string): Observable { return of({ items: [geneMock1, geneMock2] }); }