- @for (ie of item.data |sortByDate:'dataTime'; track ie.dbId) {
+ @for (ie of item.data; track $index) {
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/pathway-browser/src/app/details/tabs/description-tab/description-tab.component.ts b/projects/pathway-browser/src/app/details/tabs/description-tab/description-tab.component.ts
index 6ffd965..fd50389 100644
--- a/projects/pathway-browser/src/app/details/tabs/description-tab/description-tab.component.ts
+++ b/projects/pathway-browser/src/app/details/tabs/description-tab/description-tab.component.ts
@@ -10,7 +10,7 @@ import {
TemplateRef,
viewChild
} from '@angular/core';
-import {Analysis} from "../../../model/analysis.model";
+import type {Analysis} from "../../../model/analysis.model";
import {IconService} from "../../../services/icon.service";
import {
getProperty,
@@ -37,7 +37,7 @@ import {CatalystActivity} from "../../../model/graph/catalyst-activity.model";
import {CatalystActivityReference} from "../../../model/graph/control-reference/catalyst-activity-reference.model";
import {Regulation} from "../../../model/graph/Regulation/regulation.model";
import {RegulationReference} from "../../../model/graph/control-reference/regulation-reference.model";
-import {Relationship} from "../../../model/graph/relationship.model";
+import type {Relationship} from "../../../model/graph/relationship.model";
import {DatabaseIdentifier} from "../../../model/graph/database-identifier.model";
import {
EntityWithAccessionedSequence
@@ -49,7 +49,12 @@ import {CONTENT_DETAIL, environment} from "../../../../environments/environment"
import {SpeciesService} from "../../../services/species.service";
import {Summation} from "../../../model/graph/summation.model";
import {FigureService} from "./figure/figure.service";
-import HasModifiedResidue = Relationship.HasModifiedResidue;
+type HasModifiedResidue = Relationship.HasModifiedResidue;
+import {KeyValuePipe, NgClass, NgTemplateOutlet} from "@angular/common";
+import {RouterLink} from "@angular/router";
+import {SortByTextPipe} from "../../../pipes/sort-by-text.pipe";
+import {IncludeRefPipe} from "../../../pipes/include-ref.pipe";
+import {AuthorshipDateFormatPipe} from "../../../pipes/authorship-date-format.pipe";
import {MatDivider} from "@angular/material/divider";
import {MatIcon} from "@angular/material/icon";
import {MatTooltip} from "@angular/material/tooltip";
@@ -68,6 +73,12 @@ import {CellMarkerComponent} from "../../common/cell-marker/cell-marker.componen
import {IconComponent} from "./icon/icon.component";
import {RheaComponent} from "../../common/rhea/rhea.component";
import {InteractorsTableComponent} from "../../common/interactors-table/interactors-table.component";
+import {
+ LocationsTreeComponent
+} from "../../../../../../website-angular/src/app/content/detail/locations-tree/locations-tree.component";
+import {
+ ReactionDiagramComponent
+} from "../../common/reaction-diagram/reaction-diagram.component";
@Component({
@@ -76,6 +87,13 @@ import {InteractorsTableComponent} from "../../common/interactors-table/interact
styleUrl: './description-tab.component.scss',
standalone: true,
imports: [
+ NgTemplateOutlet,
+ NgClass,
+ KeyValuePipe,
+ RouterLink,
+ SortByTextPipe,
+ IncludeRefPipe,
+ AuthorshipDateFormatPipe,
MatDivider,
MatIcon,
MatTooltip,
@@ -92,7 +110,9 @@ import {InteractorsTableComponent} from "../../common/interactors-table/interact
CellMarkerComponent,
IconComponent,
RheaComponent,
- InteractorsTableComponent
+ InteractorsTableComponent,
+ LocationsTreeComponent,
+ ReactionDiagramComponent
]
})
export class DescriptionTabComponent implements OnDestroy {
@@ -124,6 +144,7 @@ export class DescriptionTabComponent implements OnDestroy {
readonly obj = input.required
();
readonly analysisResult = input();
+ readonly showLocations = input(false);
static referenceTypeToNameSuffix = new Map([
["ReferenceMolecule", ""],
@@ -171,21 +192,23 @@ export class DescriptionTabComponent implements OnDestroy {
referenceEntity: Signal = computed(() => getProperty(this.obj(), DataKeys.REFERENCE_ENTITY));
- readonly authorship: Signal<{ label: string, data: InstanceEdit[] }[]> = computed(() => {
+ readonly authorship: Signal<{label: string, data: InstanceEdit[]}[]> = computed(() => {
const arrayWrap = (a: E[] | E) => Array.isArray(a) ? a : [a];
const obj = this.obj();
// Ensure it's an array, either returning the existing array or wrapping it in one, it complains without this line.
- const finalAuthored = arrayWrap(getProperty(obj, DataKeys.AUTHORED) || getProperty(obj, DataKeys.CREATED) || []);
+ const authored = arrayWrap(getProperty(obj, DataKeys.AUTHORED) || []);
const reviewed = getProperty(obj, DataKeys.REVIEWED) || [];
const edited = getProperty(obj, DataKeys.EDITED) || [];
const revised = getProperty(obj, DataKeys.REVISED) || [];
+ const created = arrayWrap(getProperty(obj, DataKeys.CREATED) || []);
return [
- ...(finalAuthored.length > 0 ? [{label: Labels.AUTHOR, data: finalAuthored}] : []),
+ ...(authored.length > 0 ? [{label: Labels.AUTHOR, data: authored}] : []),
...(reviewed.length > 0 ? [{label: Labels.REVIEWER, data: reviewed}] : []),
...(edited.length > 0 ? [{label: Labels.EDITOR, data: edited}] : []),
...(revised.length > 0 ? [{label: Labels.REVISER, data: revised}] : []),
+ ...(created.length > 0 ? [{label: 'Created', data: created}] : []),
];
});
@@ -260,6 +283,10 @@ export class DescriptionTabComponent implements OnDestroy {
authorsTemplate$ = viewChild.required>('authorsTemplate');
interactorsTemplate$ = viewChild.required>('interactorsTemplate');
rheaTemplate$ = viewChild.required>('rheaTemplate');
+ locationsTemplate$ = viewChild>('locationsTemplate');
+ reactionDiagramTemplate$ = viewChild>('reactionDiagramTemplate');
+
+ readonly isReaction = computed(() => isRLE(this.obj()));
protected readonly Labels = Labels;
protected readonly DataKeys = DataKeys;
@@ -287,6 +314,20 @@ export class DescriptionTabComponent implements OnDestroy {
template: this.overviewTemplate$,
isPresent: signal(true)
},
+ {
+ key: 'locationsInPWB',
+ label: 'Locations in the Pathway Browser',
+ manual: true,
+ template: this.locationsTemplate$ as Signal>,
+ isPresent: computed(() => this.showLocations())
+ },
+ {
+ key: 'reactionDiagram',
+ label: 'Reaction Diagram',
+ manual: true,
+ template: this.reactionDiagramTemplate$ as Signal>,
+ isPresent: this.isReaction
+ },
{key: DataKeys.REFERENCE_ENTITY, label: Labels.EXTERNAL_REFERENCE, manual: true, template: this.referenceTemplate$},
{key: DataKeys.SUMMARISED_ENTITIES, label: Labels.SUMMARISED_ENTITIES},
{
@@ -437,6 +478,10 @@ export class DescriptionTabComponent implements OnDestroy {
switch (key) {
case DataKeys.OVERVIEW:
return obj;
+ case 'locationsInPWB':
+ return this.showLocations();
+ case 'reactionDiagram':
+ return this.isReaction();
case DataKeys.PROTEIN_MARKER:
return this.proteinMarkers().length + this.rnaMarkers().length > 0;
case DataKeys.CATALYST_ACTIVITY:
diff --git a/projects/pathway-browser/src/app/details/tabs/result-tab/found-table/found-table.component.ts b/projects/pathway-browser/src/app/details/tabs/result-tab/found-table/found-table.component.ts
index f8c23d5..643272e 100644
--- a/projects/pathway-browser/src/app/details/tabs/result-tab/found-table/found-table.component.ts
+++ b/projects/pathway-browser/src/app/details/tabs/result-tab/found-table/found-table.component.ts
@@ -3,7 +3,7 @@ import {ExpressionTagComponent} from "../expression-tag/expression-tag.component
import {MatTableDataSource, MatTableModule} from "@angular/material/table";
import {TypeSafeMatCellDef} from "../../../../utils/type-safe-mat-cell-def.directive";
import {TypeSafeMatRowDef} from "../../../../utils/type-safe-mat-row-def.directive";
-import {Analysis} from "../../../../model/analysis.model";
+import type {Analysis} from "../../../../model/analysis.model";
import {AnalysisService} from "../../../../services/analysis.service";
import {rxResource} from "@angular/core/rxjs-interop";
import {of} from "rxjs";
diff --git a/projects/pathway-browser/src/app/details/tabs/result-tab/not-found-table/not-found-table.component.ts b/projects/pathway-browser/src/app/details/tabs/result-tab/not-found-table/not-found-table.component.ts
index 01e987b..0230b5d 100644
--- a/projects/pathway-browser/src/app/details/tabs/result-tab/not-found-table/not-found-table.component.ts
+++ b/projects/pathway-browser/src/app/details/tabs/result-tab/not-found-table/not-found-table.component.ts
@@ -5,7 +5,7 @@ import {MatPaginatorModule} from "@angular/material/paginator";
import {MatTooltipModule} from "@angular/material/tooltip";
import {TypeSafeMatCellDef} from "../../../../utils/type-safe-mat-cell-def.directive";
import {TypeSafeMatRowDef} from "../../../../utils/type-safe-mat-row-def.directive";
-import {Analysis} from "../../../../model/analysis.model";
+import type {Analysis} from "../../../../model/analysis.model";
import {AnalysisService} from "../../../../services/analysis.service";
import {MatProgressSpinner} from "@angular/material/progress-spinner";
import {UrlStateService} from "../../../../services/url-state.service";
diff --git a/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts b/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts
index c1baa24..8cfd617 100644
--- a/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts
+++ b/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.ts
@@ -11,7 +11,7 @@ import {
} from '@angular/core';
import {AnalysisService} from "../../../services/analysis.service";
import {MatTableDataSource, MatTableModule} from "@angular/material/table";
-import {Analysis} from "../../../model/analysis.model";
+import type {Analysis} from "../../../model/analysis.model";
import {MatPaginator, MatPaginatorModule} from "@angular/material/paginator";
import {MatSort, MatSortModule, Sort} from "@angular/material/sort";
import {DecimalPipe} from "@angular/common";
diff --git a/projects/pathway-browser/src/app/diagram/diagram.component.ts b/projects/pathway-browser/src/app/diagram/diagram.component.ts
index 6dbc6c9..a838fa1 100644
--- a/projects/pathway-browser/src/app/diagram/diagram.component.ts
+++ b/projects/pathway-browser/src/app/diagram/diagram.component.ts
@@ -37,7 +37,7 @@ import {UntilDestroy} from "@ngneat/until-destroy";
import {AnalysisService} from "../services/analysis.service";
import {Graph} from "../model/graph.model";
import {average, isDefined, isPathwayWithDiagram, isReferenceEntityStId} from "../services/utils";
-import {Analysis} from "../model/analysis.model";
+import type {Analysis} from "../model/analysis.model";
import {ActivatedRoute, Router} from "@angular/router";
import {InteractorsComponent} from "../interactors/interactors.component";
import {EventService} from "../services/event.service";
diff --git a/projects/pathway-browser/src/app/model/diagram.model.ts b/projects/pathway-browser/src/app/model/diagram.model.ts
index b9d69eb..f934e9a 100644
--- a/projects/pathway-browser/src/app/model/diagram.model.ts
+++ b/projects/pathway-browser/src/app/model/diagram.model.ts
@@ -80,7 +80,7 @@ export interface NodeConnector {
type: 'INPUT' | 'OUTPUT' | 'CATALYST' | 'ACTIVATOR' | 'INHIBITOR';
segments: Segment[]
stoichiometry: { value: number }
- endShape: { centre: Position }
+ endShape: { centre: Position, c?: Position }
isFadeOut?: boolean
}
diff --git a/projects/pathway-browser/src/app/model/graph/database-object.model.ts b/projects/pathway-browser/src/app/model/graph/database-object.model.ts
index d2a457b..cbee8e4 100644
--- a/projects/pathway-browser/src/app/model/graph/database-object.model.ts
+++ b/projects/pathway-browser/src/app/model/graph/database-object.model.ts
@@ -1,5 +1,5 @@
import {InstanceEdit} from "./instance-edit.model";
-import {Relationship} from "./relationship.model";
+import type {Relationship} from "./relationship.model";
export interface DatabaseObject {
[key: string]: any;
diff --git a/projects/pathway-browser/src/app/model/graph/event/event.model.ts b/projects/pathway-browser/src/app/model/graph/event/event.model.ts
index 660bf01..fbeaa7d 100644
--- a/projects/pathway-browser/src/app/model/graph/event/event.model.ts
+++ b/projects/pathway-browser/src/app/model/graph/event/event.model.ts
@@ -3,8 +3,8 @@ import {Summation} from "../summation.model";
import {ReviewStatus} from "../review-status.model";
import {LiteratureReference} from "../publication/literature-reference.model";
import {InstanceEdit} from "../instance-edit.model";
-import {Relationship} from "../relationship.model";
-import HasCompartment = Relationship.HasCompartment;
+import type {Relationship} from "../relationship.model";
+type HasCompartment = Relationship.HasCompartment;
import {Species} from "../species.model";
import {Disease} from "../external-ontology/disease.model";
import {InDepth} from "../in-depth.model";
diff --git a/projects/pathway-browser/src/app/model/graph/event/pathway.model.ts b/projects/pathway-browser/src/app/model/graph/event/pathway.model.ts
index fbf05bf..c1f8449 100644
--- a/projects/pathway-browser/src/app/model/graph/event/pathway.model.ts
+++ b/projects/pathway-browser/src/app/model/graph/event/pathway.model.ts
@@ -1,6 +1,6 @@
import {Event} from "./event.model";
-import {Relationship} from "../relationship.model";
-import HasEvent = Relationship.HasEvent;
+import type {Relationship} from "../relationship.model";
+type HasEvent = Relationship.HasEvent;
export interface Pathway extends Event {
events: HasEvent[];
diff --git a/projects/pathway-browser/src/app/model/graph/event/reaction-like-event.model.ts b/projects/pathway-browser/src/app/model/graph/event/reaction-like-event.model.ts
index 2e6cc53..f194652 100644
--- a/projects/pathway-browser/src/app/model/graph/event/reaction-like-event.model.ts
+++ b/projects/pathway-browser/src/app/model/graph/event/reaction-like-event.model.ts
@@ -1,6 +1,6 @@
import {Event} from "./event.model";
import {PhysicalEntity} from "../physical-entity/physical-entity.model";
-import {Relationship} from "../relationship.model";
+import type {Relationship} from "../relationship.model";
import {Anatomy} from "../external-ontology/anatomy.model";
export interface ReactionLikeEvent extends Event {
diff --git a/projects/pathway-browser/src/app/model/graph/physical-entity/physical-entity.model.ts b/projects/pathway-browser/src/app/model/graph/physical-entity/physical-entity.model.ts
index 0f0527a..93e52c5 100644
--- a/projects/pathway-browser/src/app/model/graph/physical-entity/physical-entity.model.ts
+++ b/projects/pathway-browser/src/app/model/graph/physical-entity/physical-entity.model.ts
@@ -2,7 +2,7 @@ import {DatabaseObject} from "../database-object.model";
import {InstanceEdit} from "../instance-edit.model";
import {CatalystActivity} from "../catalyst-activity.model";
import {CellType} from "../external-ontology/cell-type.model";
-import {Relationship} from "../relationship.model";
+import type {Relationship} from "../relationship.model";
import {DatabaseIdentifier} from "../database-identifier.model";
import {Disease} from "../external-ontology/disease.model";
import {MarkerReference} from "../control-reference/marker-reference.model";
diff --git a/projects/pathway-browser/src/app/model/graph/physical-entity/summary-entity.model.ts b/projects/pathway-browser/src/app/model/graph/physical-entity/summary-entity.model.ts
index d61ea8d..d64ea95 100644
--- a/projects/pathway-browser/src/app/model/graph/physical-entity/summary-entity.model.ts
+++ b/projects/pathway-browser/src/app/model/graph/physical-entity/summary-entity.model.ts
@@ -1,8 +1,8 @@
import {PhysicalEntity} from "./physical-entity.model";
import {ReferenceEntity} from "../reference-entity/reference-entity.model";
import {Taxon} from "../taxon.model";
-import {Relationship} from "../relationship.model";
-import HasModifiedResidue = Relationship.HasModifiedResidue;
+import type {Relationship} from "../relationship.model";
+type HasModifiedResidue = Relationship.HasModifiedResidue;
import {ReferenceDatabase} from "../reference-database.model";
import {DatabaseIdentifier} from "../database-identifier.model";
import {ReferenceGeneProduct} from "../reference-entity/reference-gene-product.model";
diff --git a/projects/pathway-browser/src/app/reacfoam/reacfoam.service.ts b/projects/pathway-browser/src/app/reacfoam/reacfoam.service.ts
index 8cebb1a..9853f06 100644
--- a/projects/pathway-browser/src/app/reacfoam/reacfoam.service.ts
+++ b/projects/pathway-browser/src/app/reacfoam/reacfoam.service.ts
@@ -10,7 +10,7 @@ import {rxResource} from "@angular/core/rxjs-interop";
import chroma from "chroma-js";
import {AnalysisService} from "../services/analysis.service";
import {DarkService} from "../services/dark.service";
-import {Analysis} from "../model/analysis.model";
+import type {Analysis} from "../model/analysis.model";
import {extract, Style} from "reactome-cytoscape-style";
import {isArray} from "lodash";
diff --git a/projects/pathway-browser/src/app/services/analysis.service.ts b/projects/pathway-browser/src/app/services/analysis.service.ts
index d0b6c64..b8c8eea 100644
--- a/projects/pathway-browser/src/app/services/analysis.service.ts
+++ b/projects/pathway-browser/src/app/services/analysis.service.ts
@@ -2,7 +2,7 @@ import {computed, effect, inject, Injectable, linkedSignal, signal, WritableSign
import {catchError, EMPTY, Observable, of, switchMap, tap} from "rxjs";
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
-import {Analysis} from "../model/analysis.model";
+import type {Analysis} from "../model/analysis.model";
import {UrlStateService} from "./url-state.service";
import chroma, {Color, Scale} from "chroma-js";
import {extract, Style} from "reactome-cytoscape-style";
@@ -15,7 +15,7 @@ import {isDefined, shouldBeScientificFormat} from "./utils";
import {Report} from "reactome-gsa-form/lib/model/report-status.model";
import {Species} from "../model/graph/species.model";
import {SpeciesService} from "./species.service";
-import NotFoundIdentifier = Analysis.NotFoundIdentifier;
+type NotFoundIdentifier = Analysis.NotFoundIdentifier;
export interface Pagination extends Params {
page: number,
diff --git a/projects/pathway-browser/src/app/services/diagram.service.ts b/projects/pathway-browser/src/app/services/diagram.service.ts
index b05923a..a3e6ecb 100644
--- a/projects/pathway-browser/src/app/services/diagram.service.ts
+++ b/projects/pathway-browser/src/app/services/diagram.service.ts
@@ -3,7 +3,7 @@ import {catchError, forkJoin, map, Observable, of, switchMap, tap} from "rxjs";
import {HttpClient} from "@angular/common/http";
import {Diagram, Edge, Node, NodeConnector, Position, Prop, Rectangle} from "../model/diagram.model";
import {Graph} from "../model/graph.model";
-import Reactome, {Style} from "reactome-cytoscape-style";
+import {Style, Types} from "reactome-cytoscape-style";
import legend from "../../assets/json/legend.json"
import {array} from "vectorious";
@@ -12,9 +12,9 @@ import cytoscapeFcose, {FcoseLayoutOptions} from "cytoscape-fcose";
import {CONTENT_SERVICE, environment} from "../../environments/environment";
import {SchemaClasses} from "../constants/constants";
import {GeneralService} from "./general.service";
-import NodeDefinition = Reactome.Types.NodeDefinition;
-import ReactionDefinition = Reactome.Types.ReactionDefinition;
-import EdgeTypeDefinition = Reactome.Types.EdgeTypeDefinition;
+type NodeDefinition = Types.NodeDefinition;
+type ReactionDefinition = Types.ReactionDefinition;
+type EdgeTypeDefinition = Types.EdgeTypeDefinition;
cytoscape.use(cytoscapeFcose)
@@ -243,434 +243,441 @@ export class DiagramService {
)
)
})),
- map(({diagram, graph, chebiMapping}) => {
- console.log("edge.reactionType", new Set(diagram.edges.flatMap(edge => edge.reactionType)))
- console.log("node.connectors.types", new Set(diagram.nodes.flatMap(node => node.connectors.flatMap(con => con.type))))
- console.log("node.renderableClass", new Set(diagram.nodes.flatMap(node => node.renderableClass)))
- console.log("links.renderableClass", new Set(diagram.links.flatMap(link => link.renderableClass)))
- console.log("shadow.renderableClass", new Set(diagram.shadows.flatMap(shadow => shadow.renderableClass)))
-
- const idToEdges = new Map(diagram.edges.map(edge => [edge.id, edge]));
- const idToNodes = new Map(diagram.nodes.map(node => [node.id, node]));
- const reactomeIdToEdge = new Map(
- [
- // ...diagram.nodes.map(node => [node.reactomeId, node]),
- ...diagram.edges.map(edge => [edge.reactomeId, edge])
- ] as [number, Edge][]
- );
-
- const edgeIds = new Map();
- const forwardArray = diagram.edges.flatMap(edge => edge.segments.map(segment => [posToStr(edge, scale(segment.from)), scale(segment.to)])) as [string, Position][];
- this.extraLine = new Map(forwardArray);
- console.assert(forwardArray.length === this.extraLine.size, "Some edge diagram have been lost because 2 segments are starting from the same point")
-
- const backwardArray = diagram.edges.flatMap(edge => edge.segments.map(segment => [posToStr(edge, scale(segment.to)), scale(segment.from)])) as [string, Position][];
- this.reverseExtraLine = new Map(backwardArray);
- console.assert(backwardArray.length == this.reverseExtraLine.size, "Some edge diagram have been lost because 2 segments are ending at the same point")
-
-
- const subpathwayIds = new Set(diagram.shadows.map((shadow) => shadow.reactomeId))
-
- const eventIdToSubPathwayId = new Map(graph.subpathways?.flatMap(subpathway => subpathway.events
- .map(event => [event, subpathway.dbId])
- .filter(entry => subpathwayIds.has(entry[1]))) as [number, number][] || [])
-
- const subpathwayIdToEventIds = new Map(graph.subpathways?.map(subpathway => [subpathway.dbId, subpathway.events]));
- const subpathwayStIdToEventIds = new Map(graph.subpathways?.map(subpathway => [subpathway.stId, subpathway.events]));
- // create a node id - graph node mapping
- const dbIdToGraphNode = new Map(graph.nodes.map(node => ([node.dbId, node])))
- const mappingList: [number, Graph.Node][] = graph.nodes.flatMap(node => {
- if (node.children && node.children.length === 1 && node.diagramIds?.length !== 1) { // Consider homomer complex like their constituents for interactors
- return node.diagramIds?.map(id => [id, dbIdToGraphNode.get(node.children[0])])
- .filter(entry => entry[1] !== undefined) as [number, Graph.Node][]
- } else {
- return node.diagramIds?.map(id => [id, node]) as [number, Graph.Node][]
- }
- }).filter(entry => entry !== undefined);
+ map(({diagram, graph, chebiMapping}) => this.diagramFromData(diagram, graph, id, chebiMapping)),
+ tap((output) => console.log('Output:', output)),
+ )
- const idToGraphNodes = new Map([...mappingList]);
- const idToGraphEdges = new Map(graph.edges.map(edge => [edge.dbId, edge]));
+ }
- const getLeaves = (node: Graph.Node, leaves: Set) => {
- if (node.children && node.children.length > 0)
- node.children.forEach(child => getLeaves(dbIdToGraphNode.get(child)!, leaves))
- else
- leaves.add(node);
- }
+ public diagramFromData(
+ diagram: Diagram,
+ graph: Graph.Data,
+ id: number | string = '',
+ chebiMapping: Map> = new Map()
+ ): cytoscape.ElementsDefinition {
+ console.log("edge.reactionType", new Set(diagram.edges.flatMap(edge => edge.reactionType)))
+ console.log("node.connectors.types", new Set(diagram.nodes.flatMap(node => node.connectors.flatMap(con => con.type))))
+ console.log("node.renderableClass", new Set(diagram.nodes.flatMap(node => node.renderableClass)))
+ console.log("links.renderableClass", new Set(diagram.links.flatMap(link => link.renderableClass)))
+ console.log("shadow.renderableClass", new Set(diagram.shadows.flatMap(shadow => shadow.renderableClass)))
+
+ const idToEdges = new Map(diagram.edges.map(edge => [edge.id, edge]));
+ const idToNodes = new Map(diagram.nodes.map(node => [node.id, node]));
+ const reactomeIdToEdge = new Map(
+ [
+ // ...diagram.nodes.map(node => [node.reactomeId, node]),
+ ...diagram.edges.map(edge => [edge.reactomeId, edge])
+ ] as [number, Edge][]
+ );
- idToGraphNodes.forEach(node => {
- if (node.children?.length > 0) {
- let leaves = new Set();
- getLeaves(node, leaves);
- node.leaves = [...leaves];
- }
+ const edgeIds = new Map();
+ const forwardArray = diagram.edges.flatMap(edge => edge.segments.map(segment => [posToStr(edge, scale(segment.from)), scale(segment.to)])) as [string, Position][];
+ this.extraLine = new Map(forwardArray);
+ console.assert(forwardArray.length === this.extraLine.size, "Some edge diagram have been lost because 2 segments are starting from the same point")
+
+ const backwardArray = diagram.edges.flatMap(edge => edge.segments.map(segment => [posToStr(edge, scale(segment.to)), scale(segment.from)])) as [string, Position][];
+ this.reverseExtraLine = new Map(backwardArray);
+ console.assert(backwardArray.length == this.reverseExtraLine.size, "Some edge diagram have been lost because 2 segments are ending at the same point")
+
+
+ const subpathwayIds = new Set(diagram.shadows.map((shadow) => shadow.reactomeId))
+
+ const eventIdToSubPathwayId = new Map(graph.subpathways?.flatMap(subpathway => subpathway.events
+ .map(event => [event, subpathway.dbId])
+ .filter(entry => subpathwayIds.has(entry[1]))) as [number, number][] || [])
+
+ const subpathwayIdToEventIds = new Map(graph.subpathways?.map(subpathway => [subpathway.dbId, subpathway.events]));
+ const subpathwayStIdToEventIds = new Map(graph.subpathways?.map(subpathway => [subpathway.stId, subpathway.events]));
+ // create a node id - graph node mapping
+ const dbIdToGraphNode = new Map(graph.nodes.map(node => ([node.dbId, node])))
+ const mappingList: [number, Graph.Node][] = graph.nodes.flatMap(node => {
+ if (node.children && node.children.length === 1 && node.diagramIds?.length !== 1) { // Consider homomer complex like their constituents for interactors
+ return node.diagramIds?.map(id => [id, dbIdToGraphNode.get(node.children[0])])
+ .filter(entry => entry[1] !== undefined) as [number, Graph.Node][]
+ } else {
+ return node.diagramIds?.map(id => [id, node]) as [number, Graph.Node][]
+ }
+ }).filter(entry => entry !== undefined);
+
+ const idToGraphNodes = new Map([...mappingList]);
+ const idToGraphEdges = new Map(graph.edges.map(edge => [edge.dbId, edge]));
+
+ const getLeaves = (node: Graph.Node, leaves: Set) => {
+ if (node.children && node.children.length > 0)
+ node.children.forEach(child => getLeaves(dbIdToGraphNode.get(child)!, leaves))
+ else
+ leaves.add(node);
+ }
+
+ idToGraphNodes.forEach(node => {
+ if (node.children?.length > 0) {
+ let leaves = new Set();
+ getLeaves(node, leaves);
+ node.leaves = [...leaves];
+ }
+ })
+
+ const dbIdToGraphEdge = new Map(graph.edges.map(edge => ([edge.dbId, edge])))
+
+ const hasFadeOut = diagram.nodes.some(node => node.isFadeOut);
+ const normalNodes = diagram.nodes.filter(node => node.isFadeOut);
+ const specialNodes = diagram.nodes.filter(node => !node.isFadeOut);
+ const posToNormalNode = new Map(normalNodes.map(node => [pointToStr(node.position), node]));
+ const posToSpecialNode = new Map(specialNodes.map(node => [pointToStr(node.position), node]));
+
+ const normalEdges = diagram.edges.filter(edge => edge.isFadeOut);
+ const specialEdges = diagram.edges.filter(edge => !edge.isFadeOut);
+ const posToNormalEdge = new Map(normalEdges.map(edge => [pointToStr(edge.position), edge]));
+ const posToSpecialEdge = new Map(specialEdges.map(edge => [pointToStr(edge.position), edge]));
+
+ //compartment nodes
+ const compartmentNodes: cytoscape.NodeDefinition[] = diagram?.compartments.flatMap(item => {
+ const propToRects = (prop: Prop): { [p: string]: number } => ({
+ left: scale(prop.x),
+ top: scale(prop.y),
+ right: scale(prop.x + prop.width),
+ bottom: scale(prop.x + prop.height),
+ })
+
+ let innerCR = 10;
+ let outerCR
+ if (item.insets) {
+ const rects = [propToRects(item.prop), propToRects(item.insets)]
+ outerCR = Object.keys(rects[0]).reduce((smallest, key) => Math.min(smallest, Math.abs(rects[0][key] - rects[1][key])), Number.MAX_SAFE_INTEGER);
+ outerCR = innerCR + Math.min(outerCR, 100)
+ }
+
+ const layers: cytoscape.NodeDefinition[] = [
+ {
+ data: {
+ id: item.id + '-outer',
+ displayName: item.displayName,
+ textX: scale(item.textPosition.x - (item.prop.x + item.prop.width)) + this.COMPARTMENT_SHIFT,
+ textY: scale(item.textPosition.y - (item.prop.y + item.prop.height)) + this.COMPARTMENT_SHIFT,
+ width: scale(item.prop.width),
+ height: scale(item.prop.height),
+ radius: outerCR
+ },
+ classes: ['Compartment', 'outer'],
+ position: scale(item.position),
+ selectable: false,
+ }
+ ];
+
+ if (item.insets) {
+ layers.push({
+ data: {
+ id: item.id + '-inner',
+ width: scale(item.insets.width),
+ height: scale(item.insets.height),
+ radius: innerCR
+ },
+ classes: ['Compartment', 'inner'],
+ position: scale({x: item.insets.x + item.insets.width / 2, y: item.insets.y + item.insets.height / 2}),
+ selectable: false,
})
+ }
+ return layers;
+ });
- const dbIdToGraphEdge = new Map(graph.edges.map(edge => ([edge.dbId, edge])))
-
- const hasFadeOut = diagram.nodes.some(node => node.isFadeOut);
- const normalNodes = diagram.nodes.filter(node => node.isFadeOut);
- const specialNodes = diagram.nodes.filter(node => !node.isFadeOut);
- const posToNormalNode = new Map(normalNodes.map(node => [pointToStr(node.position), node]));
- const posToSpecialNode = new Map(specialNodes.map(node => [pointToStr(node.position), node]));
-
- const normalEdges = diagram.edges.filter(edge => edge.isFadeOut);
- const specialEdges = diagram.edges.filter(edge => !edge.isFadeOut);
- const posToNormalEdge = new Map(normalEdges.map(edge => [pointToStr(edge.position), edge]));
- const posToSpecialEdge = new Map(specialEdges.map(edge => [pointToStr(edge.position), edge]));
-
- //compartment nodes
- const compartmentNodes: cytoscape.NodeDefinition[] = diagram?.compartments.flatMap(item => {
- const propToRects = (prop: Prop): { [p: string]: number } => ({
- left: scale(prop.x),
- top: scale(prop.y),
- right: scale(prop.x + prop.width),
- bottom: scale(prop.x + prop.height),
- })
-
- let innerCR = 10;
- let outerCR
- if (item.insets) {
- const rects = [propToRects(item.prop), propToRects(item.insets)]
- outerCR = Object.keys(rects[0]).reduce((smallest, key) => Math.min(smallest, Math.abs(rects[0][key] - rects[1][key])), Number.MAX_SAFE_INTEGER);
- outerCR = innerCR + Math.min(outerCR, 100)
- }
+ const replacementMap = new Map();
- const layers: cytoscape.NodeDefinition[] = [
- {
- data: {
- id: item.id + '-outer',
- displayName: item.displayName,
- textX: scale(item.textPosition.x - (item.prop.x + item.prop.width)) + this.COMPARTMENT_SHIFT,
- textY: scale(item.textPosition.y - (item.prop.y + item.prop.height)) + this.COMPARTMENT_SHIFT,
- width: scale(item.prop.width),
- height: scale(item.prop.height),
- radius: outerCR
- },
- classes: ['Compartment', 'outer'],
- position: scale(item.position),
- selectable: false,
- }
- ];
+ //reaction nodes
+ const reactionNodes: cytoscape.NodeDefinition[] = diagram?.edges.map(item => {
+ let replacement, replacedBy;
+ if (item.isFadeOut) {
+ replacedBy = posToSpecialEdge.get(pointToStr(item.position))?.id.toString() || specialEdges.find(edge => squaredDist(scale(edge.position), scale(item.position)) < 5 ** 2)?.id.toString();
+ if (replacedBy) {
+ replacementMap.set(item.id.toString(), replacedBy)
+ replacementMap.set(replacedBy, item.id.toString())
+ }
+ }
+ if (!item.isFadeOut) {
+ replacement = posToNormalEdge.get(pointToStr(item.position))?.id.toString() || normalEdges.find(edge => squaredDist(scale(edge.position), scale(item.position)) < 5 ** 2)?.id.toString();
+ }
+ let subpathways = [...subpathwayStIdToEventIds.entries()].flatMap(([subpathwayId, events]) => events.includes(item.reactomeId) ? [subpathwayId] : []);
- if (item.insets) {
- layers.push({
- data: {
- id: item.id + '-inner',
- width: scale(item.insets.width),
- height: scale(item.insets.height),
- radius: innerCR
- },
- classes: ['Compartment', 'inner'],
- position: scale({x: item.insets.x + item.insets.width / 2, y: item.insets.y + item.insets.height / 2}),
- selectable: false,
- })
- }
- return layers;
- });
-
- const replacementMap = new Map();
-
- //reaction nodes
- const reactionNodes: cytoscape.NodeDefinition[] = diagram?.edges.map(item => {
- let replacement, replacedBy;
- if (item.isFadeOut) {
- replacedBy = posToSpecialEdge.get(pointToStr(item.position))?.id.toString() || specialEdges.find(edge => squaredDist(scale(edge.position), scale(item.position)) < 5 ** 2)?.id.toString();
- if (replacedBy) {
- replacementMap.set(item.id.toString(), replacedBy)
- replacementMap.set(replacedBy, item.id.toString())
- }
- }
- if (!item.isFadeOut) {
- replacement = posToNormalEdge.get(pointToStr(item.position))?.id.toString() || normalEdges.find(edge => squaredDist(scale(edge.position), scale(item.position)) < 5 ** 2)?.id.toString();
- }
- let subpathways = [...subpathwayStIdToEventIds.entries()].flatMap(([subpathwayId, events]) => events.includes(item.reactomeId) ? [subpathwayId] : []);
+ return ({
+ data: {
+ id: item.id + '',
+ // displayName: item.displayName,
+ inputs: item.inputs,
+ output: item.outputs,
+ isFadeOut: item.isFadeOut,
+ isBackground: item.isFadeOut,
+ reactomeId: item.reactomeId,
+ reactionId: item.id,
+ graph: idToGraphEdges.get(item.reactomeId),
+ subpathways: subpathways,
+ replacement, replacedBy
+ },
+ classes: this.reactionTypeMap.get(item.reactionType),
+ position: scale(item.position)
+ });
+ });
- return ({
- data: {
- id: item.id + '',
- // displayName: item.displayName,
- inputs: item.inputs,
- output: item.outputs,
- isFadeOut: item.isFadeOut,
- isBackground: item.isFadeOut,
- reactomeId: item.reactomeId,
- reactionId: item.id,
- graph: idToGraphEdges.get(item.reactomeId),
- subpathways: subpathways,
- replacement, replacedBy
- },
- classes: this.reactionTypeMap.get(item.reactionType),
- position: scale(item.position)
- });
- });
-
-
- //entity nodes
- const entityNodes: cytoscape.NodeDefinition[] = diagram?.nodes.flatMap(item => {
- let classes = [...this.nodeTypeMap.get(item.renderableClass) || item.renderableClass.toLowerCase()];
- let unitId = undefined;
- if (item.schemaClass === SchemaClasses.POLYMER) {
- const polymerGraphNode = dbIdToGraphNode.get(item.reactomeId)!;
- const unitGraph = dbIdToGraphNode.get(polymerGraphNode.children[0])!;
- const unitClass = this.nodeTypeMap.get(unitGraph.schemaClass) || this.nodeTypeMap.get(this.schemaClassToNodeTypeMap.get(unitGraph.schemaClass === 'EntityWithAccessionedSequence' ? unitGraph.referenceType : unitGraph.schemaClass)!) || ['GenomeEncodedEntity', 'PhysicalEntity'];
- classes = [SchemaClasses.POLYMER, ...unitClass]
- unitId = unitGraph.identifier;
- }
- let replacedBy: string | undefined;
- let replacement: string | undefined;
- if (item.isDisease) classes.push('disease');
- if (item.isCrossed) classes.push('crossed');
- if (item.trivial) classes.push('trivial');
- if (item.needDashedBorder) classes.push('loss-of-function');
- if (item.isFadeOut) {
- replacedBy = posToSpecialNode.get(pointToStr(item.position))?.id.toString()
- if (!replacedBy) {
- replacedBy = specialNodes.find(node => overlapLimited(item, node, 0.8))?.id.toString();
+
+ //entity nodes
+ const entityNodes: cytoscape.NodeDefinition[] = diagram?.nodes.flatMap(item => {
+ let classes = [...this.nodeTypeMap.get(item.renderableClass) || item.renderableClass.toLowerCase()];
+ let unitId = undefined;
+ if (item.schemaClass === SchemaClasses.POLYMER) {
+ const polymerGraphNode = dbIdToGraphNode.get(item.reactomeId)!;
+ const unitGraph = dbIdToGraphNode.get(polymerGraphNode.children[0])!;
+ const unitClass = this.nodeTypeMap.get(unitGraph.schemaClass) || this.nodeTypeMap.get(this.schemaClassToNodeTypeMap.get(unitGraph.schemaClass === 'EntityWithAccessionedSequence' ? unitGraph.referenceType : unitGraph.schemaClass)!) || ['GenomeEncodedEntity', 'PhysicalEntity'];
+ classes = [SchemaClasses.POLYMER, ...unitClass]
+ unitId = unitGraph.identifier;
+ }
+ let replacedBy: string | undefined;
+ let replacement: string | undefined;
+ if (item.isDisease) classes.push('disease');
+ if (item.isCrossed) classes.push('crossed');
+ if (item.trivial) classes.push('trivial');
+ if (item.needDashedBorder) classes.push('loss-of-function');
+ if (item.isFadeOut) {
+ replacedBy = posToSpecialNode.get(pointToStr(item.position))?.id.toString()
+ if (!replacedBy) {
+ replacedBy = specialNodes.find(node => overlapLimited(item, node, 0.8))?.id.toString();
+ }
+ if (replacedBy) {
+ replacementMap.set(item.id.toString(), replacedBy)
+ replacementMap.set(replacedBy, item.id.toString())
+ }
+ }
+ if (!item.isFadeOut) replacement = posToNormalNode.get(pointToStr(item.position))?.id.toString() //|| normalNodes.find(node => overlap(item, node))?.id.toString();
+ if (classes.some(clazz => clazz === 'RNA')) item.prop.height -= 10;
+ if (classes.some(clazz => clazz === 'Cell')) item.prop.height /= 2;
+ const isBackground = item.isFadeOut || classes.some(clazz => clazz === 'Pathway') || item.connectors.some(connector => connector.isFadeOut);
+ item.isBackground = isBackground;
+ let html = undefined;
+ let width = scale(item.prop.width);
+ let height = scale(item.prop.height);
+ const graphData = idToGraphNodes.get(item.id);
+ if (!graphData) console.error("Missing graph data for node: ", item.id, ". Potential reason could be a wrong normal pathway for a disease")
+ let preferredId = unitId || graphData?.identifier;
+ let chebiStructure = preferredId ? chebiMapping.get(preferredId) : undefined
+ if (classes.some(clazz => clazz === 'Protein')) {
+ html = this.getStructureVideoHtml({...item, type: 'Protein'}, width, height, preferredId);
+ }
+ if (isBackground && !item.isFadeOut) {
+ replacementMap.set(item.id.toString(), item.id.toString())
+ }
+ const isFadeOut = !item.isCrossed && item.isFadeOut;
+ const nodes: cytoscape.NodeDefinition[] = [
+ {
+ data: {
+ id: item.id + '',
+ reactomeId: item.reactomeId,
+ displayName: item.displayName.replace(/([/,:;-])/g, "$1\u200b"),
+ height: height,
+ width: width,
+ graph: graphData,
+ acc: preferredId,
+ html,
+ chebiStructure,
+ isFadeOut,
+ isBackground,
+ replacement,
+ replacedBy
+ },
+ classes: classes,
+ position: scale(item.position)
+ }
+ ];
+ if (item.nodeAttachments) {
+ nodes.push(...item.nodeAttachments.map(ptm => ({
+ data: {
+ id: item.id + '-' + ptm.reactomeId,
+ reactomeId: ptm.reactomeId,
+ nodeId: item.id,
+ nodeReactomeId: item.reactomeId,
+ displayName: ptm.label,
+ height: scale(ptm.shape.b.y - ptm.shape.a.y),
+ width: scale(ptm.shape.b.x - ptm.shape.a.x),
+ isFadeOut,
+ isBackground,
+ replacement,
+ replacedBy
+ },
+ classes: "Modification",
+ position: scale(ptm.shape.centre ?? {x: (ptm.shape.a.x + ptm.shape.b.x) / 2, y: (ptm.shape.a.y + ptm.shape.b.y) / 2})
+ })))
+ }
+ return nodes
+ });
+
+ //sub pathways
+ const shadowNodes: cytoscape.NodeDefinition[] = diagram?.shadows.map(item => {
+ return {
+ data: {
+ id: item.id + '',
+ displayName: item.displayName,
+ height: scale(item.prop.height),
+ width: scale(item.prop.width),
+ reactomeId: item.reactomeId,
+ isFadeOut: item.isFadeOut,
+ replacedBy: item.isFadeOut,
+ triggerPosition: scale(item.maxX)
+ },
+ classes: ['Shadow'],
+ position: closestToAverage(subpathwayIdToEventIds.get(item.reactomeId)!.map(reactionId => reactomeIdToEdge.get(reactionId)!).map(edge => scale(edge!.position)))
+ }
+ });
+
+ avoidOverlap(shadowNodes);
+
+ const T = 4;
+ const ARROW_MULT = 1.5;
+ const EDGE_MARGIN = 6;
+ const REACTION_RADIUS = 3 * T;
+ const MIN_DIST = EDGE_MARGIN;
+
+
+ /**
+ * Edges: iterate nodes connectors to get all edges information based on the connector type.
+ *
+ */
+ const edges: cytoscape.EdgeDefinition[] =
+ diagram.nodes.flatMap(node => {
+ return node.connectors.map(connector => {
+ const reaction = idToEdges.get(connector.edgeId)!;
+
+ const reactionP = scale(reaction.position);
+ const nodeP = scale(node.position);
+
+ const [source, target] = connector.type !== 'OUTPUT' ?
+ [node, reaction] :
+ [reaction, node];
+
+ const sourceP = scale(source.position);
+ const targetP = scale(target.position);
+
+ let points = connector.segments
+ .flatMap((segment, i) => i === 0 ? [segment.from, segment.to] : [segment.to])
+ .map(pos => scale(pos));
+ if (connector.type === 'OUTPUT') points.reverse();
+ if (points.length === 0) points.push(reactionP);
+
+ this.addEdgeInfo(reaction, points, 'backward', sourceP);
+ this.addEdgeInfo(reaction, points, 'forward', targetP);
+
+ let [from, to] = [points.shift()!, points.pop()!]
+ from = from ?? nodeP; // Quick fix to avoid problem with reaction without visible outputs like R-HSA-2424252 in R-HSA-1474244
+ to = to ?? reactionP; // Quick fix to avoid problem with reaction without visible outputs like R-HSA-2424252 in R-HSA-1474244
+ if (connector.type === 'CATALYST' && connector.endShape) {
+ to = scale(connector.endShape.centre || connector.endShape.c);
}
- if (replacedBy) {
- replacementMap.set(item.id.toString(), replacedBy)
- replacementMap.set(replacedBy, item.id.toString())
+
+ // points = addRoundness(from, to, points);
+ const relatives = this.absoluteToRelative(from, to, points);
+
+ const classes = [...this.edgeTypeMap.get(connector.type)!];
+ if (reaction.isDisease) classes.push('disease');
+ if (node.trivial) classes.push('trivial');
+ if (eventIdToSubPathwayId.has(reaction.reactomeId)) classes.push('shadow');
+
+ let subpathways = [...subpathwayStIdToEventIds.entries()].flatMap(([subpathwayId, events]) => events.includes(reaction.reactomeId) ? [subpathwayId] : []);
+
+ let d = dist(from, to);
+ if (equal(from, reactionP) || equal(to, reactionP)) d -= REACTION_RADIUS;
+ if (classes.includes('positive-regulation') || classes.includes('catalysis') || classes.includes('production')) d -= ARROW_MULT * T;
+ // console.assert(d > MIN_DIST, `The edge between reaction: R-HSA-${reaction.reactomeId} and entity: R-HSA-${node.reactomeId} in pathway ${id} has a visible length of ${d} which is shorter than ${MIN_DIST}`)
+ console.assert(d > MIN_DIST, `${id}\t${diagram.displayName}\t${hasFadeOut}\tR-HSA-${reaction.reactomeId}\tR-HSA-${node.reactomeId}\thttps://release.reactome.org/PathwayBrowser/#/${id}&SEL=R-HSA-${reaction.reactomeId}&FLG=R-HSA-${node.reactomeId}\thttps://reactome-pwp.github.io/PathwayBrowser/${id}?select=${reaction.reactomeId}&flag=${node.reactomeId}`)
+
+ let replacement, replacedBy;
+ if (connector.isFadeOut) {
+ // First case: same node is used both special and normal context
+ // replacedBy = node.connectors.find(otherConnector => otherConnector !== connector && !otherConnector.isFadeOut && samePoint(idToEdges.get(otherConnector.edgeId)!.position, reaction.position))?.edgeId;
+ // Second case: different nodes are used between special and normal context
+ // replacedBy = replacedBy || (posToSpecialNode.get(pointToStr(node.position)) && posToSpecialEdge.get(pointToStr(reaction.position)))?.id;
+
+ replacedBy = replacementMap.get(node.id.toString()) && replacementMap.get(reaction.id.toString())
}
- }
- if (!item.isFadeOut) replacement = posToNormalNode.get(pointToStr(item.position))?.id.toString() //|| normalNodes.find(node => overlap(item, node))?.id.toString();
- if (classes.some(clazz => clazz === 'RNA')) item.prop.height -= 10;
- if (classes.some(clazz => clazz === 'Cell')) item.prop.height /= 2;
- const isBackground = item.isFadeOut || classes.some(clazz => clazz === 'Pathway') || item.connectors.some(connector => connector.isFadeOut);
- item.isBackground = isBackground;
- let html = undefined;
- let width = scale(item.prop.width);
- let height = scale(item.prop.height);
- const graphData = idToGraphNodes.get(item.id);
- if (!graphData) console.error("Missing graph data for node: ", item.id, ". Potential reason could be a wrong normal pathway for a disease")
- let preferredId = unitId || graphData?.identifier;
- let chebiStructure = preferredId ? chebiMapping.get(preferredId) : undefined
- if (classes.some(clazz => clazz === 'Protein')) {
- html = this.getStructureVideoHtml({...item, type: 'Protein'}, width, height, preferredId);
- }
- if (isBackground && !item.isFadeOut) {
- replacementMap.set(item.id.toString(), item.id.toString())
- }
- const isFadeOut = !item.isCrossed && item.isFadeOut;
- const nodes: cytoscape.NodeDefinition[] = [
- {
- data: {
- id: item.id + '',
- reactomeId: item.reactomeId,
- displayName: item.displayName.replace(/([/,:;-])/g, "$1\u200b"),
- height: height,
- width: width,
- graph: graphData,
- acc: preferredId,
- html,
- chebiStructure,
- isFadeOut,
- isBackground,
- replacement,
- replacedBy
- },
- classes: classes,
- position: scale(item.position)
+ if (!connector.isFadeOut) {
+ // First case: same node is used both special and normal context
+ replacement = node.connectors.find(otherConnector => otherConnector !== connector && otherConnector.isFadeOut && samePoint(idToEdges.get(otherConnector.edgeId)!.position, reaction.position))?.edgeId;
+ // console.log("Reaction edge", replacement)
+
+ // Second case: different nodes are used between special and normal context
+ replacement = replacement || (posToNormalNode.get(pointToStr(node.position)) && posToNormalEdge.get(pointToStr(reaction.position)))?.id;
+ // console.log("Reaction edge", replacement)
+
}
- ];
- if (item.nodeAttachments) {
- nodes.push(...item.nodeAttachments.map(ptm => ({
+ const edge: cytoscape.EdgeDefinition = {
data: {
- id: item.id + '-' + ptm.reactomeId,
- reactomeId: ptm.reactomeId,
- nodeId: item.id,
- nodeReactomeId: item.reactomeId,
- displayName: ptm.label,
- height: scale(ptm.shape.b.y - ptm.shape.a.y),
- width: scale(ptm.shape.b.x - ptm.shape.a.x),
- isFadeOut,
- isBackground,
- replacement,
- replacedBy
+ id: this.getEdgeId(source, connector, target, edgeIds),
+ graph: dbIdToGraphEdge.get(reaction.reactomeId),
+ source: source.id + '',
+ target: target.id + '',
+ stoichiometry: connector.stoichiometry.value,
+ weights: relatives.weights.join(" "),
+ distances: relatives.distances.join(" "),
+ sourceEndpoint: this.endpoint(sourceP, from),
+ targetEndpoint: this.endpoint(targetP, to),
+ pathway: eventIdToSubPathwayId.get(reaction.reactomeId),
+ reactomeId: reaction.reactomeId,
+ reactionId: reaction.id,
+ isFadeOut: reaction.isFadeOut,
+ isBackground: reaction.isFadeOut,
+ subpathways,
+ replacedBy, replacement
},
- classes: "Modification",
- position: scale(ptm.shape.centre)
- })))
- }
- return nodes
- });
+ classes: classes
+ };
+ return edge
+ });
+ }
+ );
+
+ const linkEdges: cytoscape.EdgeDefinition[] = diagram.links
+ ?.filter(link => !link.renderableClass.includes('EntitySet') || link.inputs[0].id !== link.outputs[0].id)
+ ?.map(link => {
+ const source = idToNodes.get(link.inputs[0].id)!;
+ const target = idToNodes.get(link.outputs[0].id)!;
+
+ const sourceP = scale(source.position);
+ const targetP = scale(target.position);
+
+ let points = link.segments
+ .flatMap((segment, i) => i === 0 ? [segment.from, segment.to] : [segment.to])
+ .map(pos => scale(pos));
+
+ let [from, to] = [points.shift()!, points.pop()!]
+ from = from ?? sourceP; // Quick fix to avoid problem with reaction without visible outputs like R-HSA-2424252 in R-HSA-1474244
+ to = to ?? targetP; // Quick fix to avoid problem with reaction without visible outputs like R-HSA-2424252 in R-HSA-1474244
+
+ // points = addRoundness(from, to, points);
+ const relatives = this.absoluteToRelative(from, to, points);
+
+ const classes = [...this.linkClassMap.get(link.renderableClass)!];
+ if (link.isDisease) classes.push('disease');
+ const isBackground = link.isFadeOut ||
+ idToNodes.get(link.inputs[0].id)?.isBackground &&
+ idToNodes.get(link.outputs[0].id)?.isBackground;
- //sub pathways
- const shadowNodes: cytoscape.NodeDefinition[] = diagram?.shadows.map(item => {
return {
data: {
- id: item.id + '',
- displayName: item.displayName,
- height: scale(item.prop.height),
- width: scale(item.prop.width),
- reactomeId: item.reactomeId,
- isFadeOut: item.isFadeOut,
- replacedBy: item.isFadeOut,
- triggerPosition: scale(item.maxX)
+ id: link.id + '',
+ source: link.inputs[0].id + '',
+ target: link.outputs[0].id + '',
+ weights: relatives.weights.join(" "),
+ distances: relatives.distances.join(" "),
+ sourceEndpoint: this.endpoint(sourceP, from),
+ targetEndpoint: this.endpoint(targetP, to),
+ isFadeOut: link.isFadeOut,
+ isBackground: isBackground
},
- classes: ['Shadow'],
- position: closestToAverage(subpathwayIdToEventIds.get(item.reactomeId)!.map(reactionId => reactomeIdToEdge.get(reactionId)!).map(edge => scale(edge!.position)))
+ classes: classes,
+ selectable: false
}
- });
-
- avoidOverlap(shadowNodes);
-
- const T = 4;
- const ARROW_MULT = 1.5;
- const EDGE_MARGIN = 6;
- const REACTION_RADIUS = 3 * T;
- const MIN_DIST = EDGE_MARGIN;
-
-
- /**
- * Edges: iterate nodes connectors to get all edges information based on the connector type.
- *
- */
- const edges: cytoscape.EdgeDefinition[] =
- diagram.nodes.flatMap(node => {
- return node.connectors.map(connector => {
- const reaction = idToEdges.get(connector.edgeId)!;
-
- const reactionP = scale(reaction.position);
- const nodeP = scale(node.position);
-
- const [source, target] = connector.type !== 'OUTPUT' ?
- [node, reaction] :
- [reaction, node];
-
- const sourceP = scale(source.position);
- const targetP = scale(target.position);
-
- let points = connector.segments
- .flatMap((segment, i) => i === 0 ? [segment.from, segment.to] : [segment.to])
- .map(pos => scale(pos));
- if (connector.type === 'OUTPUT') points.reverse();
- if (points.length === 0) points.push(reactionP);
-
- this.addEdgeInfo(reaction, points, 'backward', sourceP);
- this.addEdgeInfo(reaction, points, 'forward', targetP);
-
- let [from, to] = [points.shift()!, points.pop()!]
- from = from ?? nodeP; // Quick fix to avoid problem with reaction without visible outputs like R-HSA-2424252 in R-HSA-1474244
- to = to ?? reactionP; // Quick fix to avoid problem with reaction without visible outputs like R-HSA-2424252 in R-HSA-1474244
- if (connector.type === 'CATALYST') {
- to = scale(connector.endShape.centre);
- }
-
- // points = addRoundness(from, to, points);
- const relatives = this.absoluteToRelative(from, to, points);
-
- const classes = [...this.edgeTypeMap.get(connector.type)!];
- if (reaction.isDisease) classes.push('disease');
- if (node.trivial) classes.push('trivial');
- if (eventIdToSubPathwayId.has(reaction.reactomeId)) classes.push('shadow');
-
- let subpathways = [...subpathwayStIdToEventIds.entries()].flatMap(([subpathwayId, events]) => events.includes(reaction.reactomeId) ? [subpathwayId] : []);
-
- let d = dist(from, to);
- if (equal(from, reactionP) || equal(to, reactionP)) d -= REACTION_RADIUS;
- if (classes.includes('positive-regulation') || classes.includes('catalysis') || classes.includes('production')) d -= ARROW_MULT * T;
- // console.assert(d > MIN_DIST, `The edge between reaction: R-HSA-${reaction.reactomeId} and entity: R-HSA-${node.reactomeId} in pathway ${id} has a visible length of ${d} which is shorter than ${MIN_DIST}`)
- console.assert(d > MIN_DIST, `${id}\t${diagram.displayName}\t${hasFadeOut}\tR-HSA-${reaction.reactomeId}\tR-HSA-${node.reactomeId}\thttps://release.reactome.org/PathwayBrowser/#/${id}&SEL=R-HSA-${reaction.reactomeId}&FLG=R-HSA-${node.reactomeId}\thttps://reactome-pwp.github.io/PathwayBrowser/${id}?select=${reaction.reactomeId}&flag=${node.reactomeId}`)
-
- let replacement, replacedBy;
- if (connector.isFadeOut) {
- // First case: same node is used both special and normal context
- // replacedBy = node.connectors.find(otherConnector => otherConnector !== connector && !otherConnector.isFadeOut && samePoint(idToEdges.get(otherConnector.edgeId)!.position, reaction.position))?.edgeId;
- // Second case: different nodes are used between special and normal context
- // replacedBy = replacedBy || (posToSpecialNode.get(pointToStr(node.position)) && posToSpecialEdge.get(pointToStr(reaction.position)))?.id;
-
- replacedBy = replacementMap.get(node.id.toString()) && replacementMap.get(reaction.id.toString())
- }
- if (!connector.isFadeOut) {
- // First case: same node is used both special and normal context
- replacement = node.connectors.find(otherConnector => otherConnector !== connector && otherConnector.isFadeOut && samePoint(idToEdges.get(otherConnector.edgeId)!.position, reaction.position))?.edgeId;
- // console.log("Reaction edge", replacement)
-
- // Second case: different nodes are used between special and normal context
- replacement = replacement || (posToNormalNode.get(pointToStr(node.position)) && posToNormalEdge.get(pointToStr(reaction.position)))?.id;
- // console.log("Reaction edge", replacement)
-
- }
- const edge: cytoscape.EdgeDefinition = {
- data: {
- id: this.getEdgeId(source, connector, target, edgeIds),
- graph: dbIdToGraphEdge.get(reaction.reactomeId),
- source: source.id + '',
- target: target.id + '',
- stoichiometry: connector.stoichiometry.value,
- weights: relatives.weights.join(" "),
- distances: relatives.distances.join(" "),
- sourceEndpoint: this.endpoint(sourceP, from),
- targetEndpoint: this.endpoint(targetP, to),
- pathway: eventIdToSubPathwayId.get(reaction.reactomeId),
- reactomeId: reaction.reactomeId,
- reactionId: reaction.id,
- isFadeOut: reaction.isFadeOut,
- isBackground: reaction.isFadeOut,
- subpathways,
- replacedBy, replacement
- },
- classes: classes
- };
- return edge
- });
- }
- );
-
- const linkEdges: cytoscape.EdgeDefinition[] = diagram.links
- ?.filter(link => !link.renderableClass.includes('EntitySet') || link.inputs[0].id !== link.outputs[0].id)
- ?.map(link => {
- const source = idToNodes.get(link.inputs[0].id)!;
- const target = idToNodes.get(link.outputs[0].id)!;
-
- const sourceP = scale(source.position);
- const targetP = scale(target.position);
-
- let points = link.segments
- .flatMap((segment, i) => i === 0 ? [segment.from, segment.to] : [segment.to])
- .map(pos => scale(pos));
-
- let [from, to] = [points.shift()!, points.pop()!]
- from = from ?? sourceP; // Quick fix to avoid problem with reaction without visible outputs like R-HSA-2424252 in R-HSA-1474244
- to = to ?? targetP; // Quick fix to avoid problem with reaction without visible outputs like R-HSA-2424252 in R-HSA-1474244
-
- // points = addRoundness(from, to, points);
- const relatives = this.absoluteToRelative(from, to, points);
-
- const classes = [...this.linkClassMap.get(link.renderableClass)!];
- if (link.isDisease) classes.push('disease');
- const isBackground = link.isFadeOut ||
- idToNodes.get(link.inputs[0].id)?.isBackground &&
- idToNodes.get(link.outputs[0].id)?.isBackground;
-
- return {
- data: {
- id: link.id + '',
- source: link.inputs[0].id + '',
- target: link.outputs[0].id + '',
- weights: relatives.weights.join(" "),
- distances: relatives.distances.join(" "),
- sourceEndpoint: this.endpoint(sourceP, from),
- targetEndpoint: this.endpoint(targetP, to),
- isFadeOut: link.isFadeOut,
- isBackground: isBackground
- },
- classes: classes,
- selectable: false
- }
- }
- )
-
- console.log('All data created')
- return {
- nodes: [...compartmentNodes, ...reactionNodes, ...entityNodes, ...shadowNodes],
- edges: [...edges, ...linkEdges]
- };
- }),
- tap((output) => console.log('Output:', output)),
- )
+ }
+ )
+ console.log('All data created')
+ return {
+ nodes: [...compartmentNodes, ...reactionNodes, ...entityNodes, ...shadowNodes],
+ edges: [...edges, ...linkEdges]
+ };
}
getStructureVideoHtml(item: {
@@ -705,18 +712,31 @@ export class DiagramService {
private addEdgeInfo(edge: Edge, points: Position[], direction: 'forward' | 'backward', stop: Position) {
const stopPos = posToStr(edge, stop);
+ const visited = new Set();
if (direction === 'forward') {
- const map = this.extraLine;
let pos = posToStr(edge, points.at(-1)!)
- while (map.has(pos) && pos !== stopPos) {
- points.push(map.get(pos)!)
+ while (pos !== stopPos && !visited.has(pos)) {
+ visited.add(pos);
+ if (this.extraLine.has(pos)) {
+ points.push(this.extraLine.get(pos)!)
+ } else if (this.reverseExtraLine.has(pos)) {
+ points.push(this.reverseExtraLine.get(pos)!)
+ } else {
+ break;
+ }
pos = posToStr(edge, points.at(-1)!)
}
} else {
- const map = this.reverseExtraLine;
let pos = posToStr(edge, points.at(0)!)
- while (map.has(pos) && pos !== stopPos) {
- points.unshift(map.get(pos)!)
+ while (pos !== stopPos && !visited.has(pos)) {
+ visited.add(pos);
+ if (this.reverseExtraLine.has(pos)) {
+ points.unshift(this.reverseExtraLine.get(pos)!)
+ } else if (this.extraLine.has(pos)) {
+ points.unshift(this.extraLine.get(pos)!)
+ } else {
+ break;
+ }
pos = posToStr(edge, points.at(0)!)
}
}
diff --git a/projects/pathway-browser/src/app/services/ehld.service.ts b/projects/pathway-browser/src/app/services/ehld.service.ts
index 512903d..2a38662 100644
--- a/projects/pathway-browser/src/app/services/ehld.service.ts
+++ b/projects/pathway-browser/src/app/services/ehld.service.ts
@@ -1,7 +1,7 @@
import {computed, ElementRef, Injectable} from '@angular/core';
import {Observable} from "rxjs";
import {HttpClient} from "@angular/common/http";
-import {Analysis} from "../model/analysis.model";
+import type {Analysis} from "../model/analysis.model";
import {isArray} from "lodash";
import {AnalysisService} from "./analysis.service";
import {DataStateService} from "./data-state.service";
diff --git a/projects/pathway-browser/src/app/services/event.service.ts b/projects/pathway-browser/src/app/services/event.service.ts
index a96ebcf..49522a7 100644
--- a/projects/pathway-browser/src/app/services/event.service.ts
+++ b/projects/pathway-browser/src/app/services/event.service.ts
@@ -19,7 +19,7 @@ import {
import {UrlStateService} from "./url-state.service";
import {Event} from "../model/graph/event/event.model";
import {MatTree} from "@angular/material/tree";
-import {Analysis} from "../model/analysis.model";
+import type {Analysis} from "../model/analysis.model";
import {AnalysisService} from "./analysis.service";
import {EhldService} from "./ehld.service";
import {TopLevelPathway} from "../model/graph/event/top-level-pathway.model";
@@ -27,10 +27,10 @@ import {DatabaseObject} from "../model/graph/database-object.model";
import {isDefined, isPathway, isPhysicalEntity, isRLE} from "./utils";
import {DatabaseObjectService} from "./database-object.service";
import {PhysicalEntity} from "../model/graph/physical-entity/physical-entity.model";
-import {Relationship} from "../model/graph/relationship.model";
+import type {Relationship} from "../model/graph/relationship.model";
import {toObservable} from "@angular/core/rxjs-interop";
import {Pathway} from "../model/graph/event/pathway.model";
-import HasEvent = Relationship.HasEvent;
+type HasEvent = Relationship.HasEvent;
import {SummaryEntity} from "../model/graph/physical-entity/summary-entity.model";
diff --git a/projects/pathway-browser/src/app/services/url-state.service.ts b/projects/pathway-browser/src/app/services/url-state.service.ts
index 4c3f036..238bcd4 100644
--- a/projects/pathway-browser/src/app/services/url-state.service.ts
+++ b/projects/pathway-browser/src/app/services/url-state.service.ts
@@ -5,7 +5,7 @@ import {isArray, isNumber} from "lodash";
import {HttpClient} from "@angular/common/http";
import {CONTENT_SERVICE} from "../../environments/environment";
import {PaletteName} from "./analysis.service";
-import {Analysis} from "../model/analysis.model";
+import type {Analysis} from "../model/analysis.model";
import {UntilDestroy, untilDestroyed} from "@ngneat/until-destroy";
import {toSignal} from "@angular/core/rxjs-interop";
diff --git a/projects/pathway-browser/src/app/services/utils.ts b/projects/pathway-browser/src/app/services/utils.ts
index bf0cd21..2e4cc02 100644
--- a/projects/pathway-browser/src/app/services/utils.ts
+++ b/projects/pathway-browser/src/app/services/utils.ts
@@ -10,7 +10,7 @@ import {LiteratureReference} from "../model/graph/publication/literature-referen
import {SchemaClasses} from "../constants/constants";
import {CatalystActivity} from "../model/graph/catalyst-activity.model";
import {Regulation} from "../model/graph/Regulation/regulation.model";
-import {Relationship} from "../model/graph/relationship.model";
+import type {Relationship} from "../model/graph/relationship.model";
import {ReferenceGroup} from "../model/graph/reference-entity/reference-group.model";
import {ReplacedResidue} from "../model/graph/abstract-modified-residue/replaced-residue.model";
import {FragmentModification} from "../model/graph/abstract-modified-residue/fragment-modification.model";
@@ -24,7 +24,7 @@ import {SummaryEntity} from "../model/graph/physical-entity/summary-entity.model
import {ReferenceSequence} from "../model/graph/reference-entity/reference-sequence.model";
import {ReferenceGeneProduct} from "../model/graph/reference-entity/reference-gene-product.model";
import {WritableSignal} from "@angular/core";
-import HasModifiedResidue = Relationship.HasModifiedResidue;
+type HasModifiedResidue = Relationship.HasModifiedResidue;
export function isDefined(value: T | undefined | null): value is T {
return value !== undefined && value !== null
diff --git a/projects/pathway-browser/src/app/viewport/analysis-form/tissue-analysis/tissue-analysis.component.ts b/projects/pathway-browser/src/app/viewport/analysis-form/tissue-analysis/tissue-analysis.component.ts
index 9acb362..1b6ad57 100644
--- a/projects/pathway-browser/src/app/viewport/analysis-form/tissue-analysis/tissue-analysis.component.ts
+++ b/projects/pathway-browser/src/app/viewport/analysis-form/tissue-analysis/tissue-analysis.component.ts
@@ -3,7 +3,7 @@ import {MatFormField, MatLabel, MatOption, MatSelect} from "@angular/material/se
import {TissueExperimentService} from "./tissue-experiment/tissue-experiment.service";
import {MatProgressSpinner} from "@angular/material/progress-spinner";
import {MatTooltip} from "@angular/material/tooltip";
-import {TissueExperiment} from "./tissue-experiment/tissue-experiment.model";
+import type {TissueExperiment} from "./tissue-experiment/tissue-experiment.model";
import {AnalysisService} from "../../../services/analysis.service";
import type {DotLottie} from "@lottiefiles/dotlottie-web";
import {LottieService} from "../../../services/lottie.service";
@@ -12,7 +12,7 @@ import {MatButton, MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {add} from "vectorious";
import {animate, group, sequence, style, transition, trigger} from "@angular/animations";
-import Summary = TissueExperiment.Summary;
+type Summary = TissueExperiment.Summary;
import {MatStep, MatStepper, MatStepperNext, MatStepperPrevious} from "@angular/material/stepper";
import {FormBuilder, FormControl} from "@angular/forms";
import {AsyncPipe} from "@angular/common";
diff --git a/projects/pathway-browser/src/app/viewport/analysis-form/tissue-analysis/tissue-experiment/tissue-experiment.service.ts b/projects/pathway-browser/src/app/viewport/analysis-form/tissue-analysis/tissue-experiment/tissue-experiment.service.ts
index 94d28db..9c90420 100644
--- a/projects/pathway-browser/src/app/viewport/analysis-form/tissue-analysis/tissue-experiment/tissue-experiment.service.ts
+++ b/projects/pathway-browser/src/app/viewport/analysis-form/tissue-analysis/tissue-experiment/tissue-experiment.service.ts
@@ -1,6 +1,6 @@
import {Injectable, ResourceRef} from '@angular/core';
import {HttpClient} from "@angular/common/http";
-import {TissueExperiment} from "./tissue-experiment.model";
+import type {TissueExperiment} from "./tissue-experiment.model";
import {Observable} from "rxjs";
import {environment} from "../../../../../environments/environment";
import {rxResource} from "@angular/core/rxjs-interop";
diff --git a/projects/website-angular/src/app/app.routes.ts b/projects/website-angular/src/app/app.routes.ts
index 42b5bf2..517b140 100644
--- a/projects/website-angular/src/app/app.routes.ts
+++ b/projects/website-angular/src/app/app.routes.ts
@@ -36,6 +36,11 @@ export const routes: Routes = [
{ path: 'content/schema', loadComponent: () => import('./content/schema/schema.component').then(m => m.SchemaComponent), pathMatch: 'full' },
{ path: 'content/schema/:className', loadComponent: () => import('./content/schema/schema.component').then(m => m.SchemaComponent), pathMatch: 'full' },
+ //Detail Pages
+ { path: 'content/detail/interactor/:acc', loadComponent: () => import('./content/detail/interactor-detail/interactor-detail.component').then(m => m.InteractorDetailComponent) },
+ { path: 'content/detail/icon/:id', loadComponent: () => import('./content/detail/icon-detail/icon-detail.component').then(m => m.IconDetailComponent) },
+ { path: 'content/detail/:id', loadComponent: () => import('./content/detail/detail.component').then(m => m.DetailComponent) },
+
//Search Pages
{ path: 'content/query', loadComponent: () => import('./search/search.component').then(m => m.SearchComponent) },
{ path: 'tools/site-search', loadComponent: () => import('./site-search/site-search.component').then(m => m.SiteSearchComponent) },
diff --git a/projects/website-angular/src/app/content/detail/detail.component.html b/projects/website-angular/src/app/content/detail/detail.component.html
new file mode 100644
index 0000000..a8dc5ed
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/detail.component.html
@@ -0,0 +1,16 @@
+
+ @if (loading()) {
+
+
+
+ } @else if (error()) {
+
+
Entity not found
+
The requested entity could not be found.
+
+ } @else if (obj()) {
+
+
+
+ }
+
diff --git a/projects/website-angular/src/app/content/detail/detail.component.scss b/projects/website-angular/src/app/content/detail/detail.component.scss
new file mode 100644
index 0000000..6d8f267
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/detail.component.scss
@@ -0,0 +1,63 @@
+:host {
+ display: block;
+}
+
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+}
+
+.error-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 300px;
+ color: var(--on-surface);
+
+ h2 {
+ margin-bottom: 8px;
+ }
+
+ p {
+ color: var(--outline);
+ }
+}
+
+.detail-container {
+ container: bottom-panel / inline-size;
+ width: 100%;
+}
+
+::ng-deep cr-description-tab {
+ // Double the ID selector to beat Angular's encapsulated
+ // #details[_ngcontent-xxx] specificity (0,2,0) with (0,2,1)
+ #details#details {
+ height: auto;
+ overflow-y: visible;
+ overflow-x: visible;
+ padding: 0;
+ background: unset;
+ }
+
+ #details {
+ .content {
+ min-width: 0;
+ overflow-x: auto;
+ }
+
+ .details-button {
+ display: none;
+ }
+
+ .toc {
+ height: auto;
+ max-height: calc(100vh - 100px);
+ overflow-y: auto;
+ position: sticky;
+ top: 80px;
+ }
+ }
+}
diff --git a/projects/website-angular/src/app/content/detail/detail.component.ts b/projects/website-angular/src/app/content/detail/detail.component.ts
new file mode 100644
index 0000000..8184079
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/detail.component.ts
@@ -0,0 +1,110 @@
+import {Component, inject, OnInit, signal} from '@angular/core';
+import {DatePipe} from '@angular/common';
+import {ActivatedRoute} from '@angular/router';
+import {DomSanitizer} from '@angular/platform-browser';
+import {MatIconRegistry} from '@angular/material/icon';
+import {MatProgressSpinner} from '@angular/material/progress-spinner';
+import {of} from 'rxjs';
+
+import {PageLayoutComponent} from '../../page-layout/page-layout.component';
+import {
+ DescriptionTabComponent
+} from '../../../../../pathway-browser/src/app/details/tabs/description-tab/description-tab.component';
+import {SelectableObject} from '../../../../../pathway-browser/src/app/services/event.service';
+import {UrlStateService} from '../../../../../pathway-browser/src/app/services/url-state.service';
+import {DataStateService} from '../../../../../pathway-browser/src/app/services/data-state.service';
+import {EntityService} from '../../../../../pathway-browser/src/app/services/entity.service';
+import {InteractorService} from '../../../../../pathway-browser/src/app/interactors/services/interactor.service';
+import {FigureService} from '../../../../../pathway-browser/src/app/details/tabs/description-tab/figure/figure.service';
+import {SpeciesService} from '../../../../../pathway-browser/src/app/services/species.service';
+import {DiagramService} from '../../../../../pathway-browser/src/app/services/diagram.service';
+import {ParticipantService} from '../../../../../pathway-browser/src/app/services/participant.service';
+import {IconService} from '../../../../../pathway-browser/src/app/services/icon.service';
+
+import {DetailDataService} from '../../../services/detail-data.service';
+import {DetailUrlState} from './providers/detail-url-state.provider';
+import {DetailDataState} from './providers/detail-data-state.provider';
+import {DetailEntityService} from './providers/detail-entity.provider';
+import {DetailInteractorService} from './providers/detail-interactor.provider';
+import {DetailFigureService} from './providers/detail-figure.provider';
+import {DetailSpeciesService} from './providers/detail-species.provider';
+
+@Component({
+ selector: 'app-detail',
+ standalone: true,
+ imports: [PageLayoutComponent, DescriptionTabComponent, MatProgressSpinner],
+ providers: [
+ {provide: UrlStateService, useClass: DetailUrlState},
+ {provide: DataStateService, useClass: DetailDataState},
+ {provide: EntityService, useClass: DetailEntityService},
+ {provide: InteractorService, useClass: DetailInteractorService},
+ {provide: FigureService, useClass: DetailFigureService},
+ {provide: SpeciesService, useClass: DetailSpeciesService},
+ DiagramService,
+ {provide: ParticipantService, useValue: {getReferenceEntities: () => of([])}},
+ DatePipe,
+ ],
+ templateUrl: './detail.component.html',
+ styleUrl: './detail.component.scss',
+})
+export class DetailComponent implements OnInit {
+ private route = inject(ActivatedRoute);
+ private detailDataService = inject(DetailDataService);
+ private dataState = inject(DataStateService) as unknown as DetailDataState;
+ private matIconRegistry = inject(MatIconRegistry);
+ private domSanitizer = inject(DomSanitizer);
+ private iconService = inject(IconService);
+
+ obj = signal(undefined);
+ loading = signal(true);
+ error = signal(false);
+
+ constructor() {
+ this.registerIcons();
+ }
+
+ private registerIcons() {
+ const speciesIcons = this.iconService.getSpeciesIcons();
+ const generalIcons = this.iconService.getGeneralIcons();
+ const reactomeSubjectIcons = this.iconService.getReactomeSubjectIcons();
+
+ this.matIconRegistry.registerFontClassAlias('symbols', 'material-symbols-rounded');
+
+ speciesIcons.forEach(icon => {
+ this.matIconRegistry.addSvgIcon(icon.name, this.domSanitizer.bypassSecurityTrustResourceUrl(`assets/icons/species/${icon.route}.svg`));
+ });
+
+ generalIcons.forEach(icon => {
+ this.matIconRegistry.addSvgIcon(icon.name, this.domSanitizer.bypassSecurityTrustResourceUrl(`assets/icons/general/${icon.route}.svg`));
+ });
+
+ Object.values(reactomeSubjectIcons).forEach((icon) => {
+ this.matIconRegistry.addSvgIcon(icon.name, this.domSanitizer.bypassSecurityTrustResourceUrl(`assets/icons/reactome-subject/${icon.route}.svg`));
+ });
+ }
+
+ ngOnInit() {
+ const id = this.route.snapshot.paramMap.get('id');
+ if (!id) {
+ this.loading.set(false);
+ this.error.set(true);
+ return;
+ }
+
+ this.detailDataService.fetchEnhancedData(id).subscribe({
+ next: (data) => {
+ if (data) {
+ this.obj.set(data);
+ this.dataState.selectedElement.set(data);
+ } else {
+ this.error.set(true);
+ }
+ this.loading.set(false);
+ },
+ error: () => {
+ this.error.set(true);
+ this.loading.set(false);
+ }
+ });
+ }
+}
diff --git a/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.html b/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.html
new file mode 100644
index 0000000..7f96191
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.html
@@ -0,0 +1,117 @@
+
+ @if (loading()) {
+
+
+
+ } @else if (error()) {
+
+
Icon not found
+
The requested icon could not be found.
+
+ } @else if (icon()) {
+
+
+
+
+
+ @if (icon()!.summation) {
+
+ Description
+
+
+ }
+
+ @if (icon()!.iconCuratorName || icon()!.iconDesignerName) {
+
+ Credits
+
+ @if (icon()!.iconCuratorName) {
+
+ }
+ @if (icon()!.iconDesignerName) {
+
+ }
+
+
+ }
+
+ @if (getUniProtReferences().length) {
+
+ External References
+
+
+ }
+
+ @if (icon()!.iconPhysicalEntities?.length) {
+
+ Physical Entities ({{ icon()!.iconPhysicalEntities.length }})
+
+
+
+
+ | Name |
+ Identifier |
+ Type |
+ Compartment |
+
+
+
+ @for (entity of icon()!.iconPhysicalEntities; track entity.stId) {
+
+ |
+ {{ entity.displayName || entity.name }}
+ |
+ {{ entity.stId }} |
+ {{ entity.type }} |
+ {{ entity.compartments }} |
+
+ }
+
+
+
+
+ }
+
+ }
+
diff --git a/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.scss b/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.scss
new file mode 100644
index 0000000..bad649b
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.scss
@@ -0,0 +1,178 @@
+:host {
+ display: block;
+}
+
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+}
+
+.error-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 300px;
+ color: var(--on-surface);
+
+ h2 {
+ margin-bottom: 8px;
+ }
+
+ p {
+ color: var(--outline);
+ }
+}
+
+.icon-detail {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.icon-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 24px;
+ flex-wrap: wrap;
+
+ .icon-info {
+ flex: 1;
+ min-width: 200px;
+
+ h1 {
+ margin: 0 0 12px;
+ font-size: 1.5rem;
+ }
+ }
+
+ .icon-preview {
+ flex-shrink: 0;
+ width: 120px;
+ height: 120px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--outline, #ddd);
+ border-radius: 8px;
+ padding: 12px;
+ background: white;
+
+ img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ }
+ }
+}
+
+.icon-categories {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.category-tag {
+ display: inline-block;
+ padding: 4px 12px;
+ border-radius: 16px;
+ background: var(--surface, #f0f0f0);
+ font-size: 0.85rem;
+ text-transform: capitalize;
+}
+
+h2 {
+ margin: 0 0 12px;
+ font-size: 1.2rem;
+}
+
+.credits {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.credit-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .credit-label {
+ font-weight: 600;
+ }
+
+ a {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ .material-symbols-rounded {
+ font-size: 16px;
+ }
+ }
+}
+
+.references {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.ref-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ .material-symbols-rounded {
+ font-size: 16px;
+ }
+}
+
+.table-wrapper {
+ overflow-x: auto;
+}
+
+.entities-table {
+ width: 100%;
+ border-collapse: collapse;
+
+ th, td {
+ padding: 10px 14px;
+ text-align: left;
+ border-bottom: 1px solid var(--outline, #ddd);
+ }
+
+ th {
+ font-weight: 600;
+ background: var(--surface, #f5f5f5);
+ white-space: nowrap;
+ }
+
+ tbody tr:hover {
+ background: rgba(0, 0, 0, 0.02);
+ }
+
+ a {
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+}
diff --git a/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.ts b/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.ts
new file mode 100644
index 0000000..e8cb120
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/icon-detail/icon-detail.component.ts
@@ -0,0 +1,52 @@
+import {Component, inject, OnInit, signal} from '@angular/core';
+import {ActivatedRoute, RouterLink} from '@angular/router';
+import {MatProgressSpinner} from '@angular/material/progress-spinner';
+import {PageLayoutComponent} from '../../../page-layout/page-layout.component';
+import {TileComponent} from '../../../reactome-components/tile/tile.component';
+import {IconService, IconEntry} from '../../../../services/icon.service';
+
+@Component({
+ selector: 'app-icon-detail',
+ standalone: true,
+ imports: [PageLayoutComponent, TileComponent, MatProgressSpinner, RouterLink],
+ templateUrl: './icon-detail.component.html',
+ styleUrl: './icon-detail.component.scss',
+})
+export class IconDetailComponent implements OnInit {
+ private route = inject(ActivatedRoute);
+ private iconService = inject(IconService);
+
+ icon = signal(null);
+ loading = signal(true);
+ error = signal(false);
+
+ ngOnInit() {
+ const id = this.route.snapshot.paramMap.get('id');
+ if (!id) {
+ this.loading.set(false);
+ this.error.set(true);
+ return;
+ }
+
+ this.iconService.getIcon(id).subscribe({
+ next: (data) => {
+ this.icon.set(data);
+ this.loading.set(false);
+ },
+ error: () => {
+ this.error.set(true);
+ this.loading.set(false);
+ },
+ });
+ }
+
+ getSvgUrl(): string {
+ const stId = this.icon()?.stId;
+ return stId ? `https://reactome.org/icon/${stId}.svg` : '';
+ }
+
+ getUniProtReferences(): string[] {
+ const refs = this.icon()?.iconReferences ?? [];
+ return refs.filter(r => !r.includes(':'));
+ }
+}
diff --git a/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.html b/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.html
new file mode 100644
index 0000000..a1efee2
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.html
@@ -0,0 +1,91 @@
+
+ @if (loading()) {
+
+
+
+ } @else if (error()) {
+
+
Interactor not found
+
No interaction data could be found for "{{ acc() }}".
+
+ } @else {
+
+
+
+
+
+
+
- Type
+ - {{ interactorType() }}
+
+ @if (species()) {
+
+
- Species
+ - {{ species() }}
+
+ }
+ @if (synonyms().length) {
+
+
- Synonyms
+ - {{ synonyms().join(', ') }}
+
+ }
+
+
+
+
+ @if (interactions().length) {
+
+ Interaction Partners ({{ interactions().length }})
+
+
+ } @else {
+
+ No interaction partners found.
+
+ }
+
+ }
+
diff --git a/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.scss b/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.scss
new file mode 100644
index 0000000..8442555
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.scss
@@ -0,0 +1,135 @@
+:host {
+ display: block;
+}
+
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+}
+
+.error-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 300px;
+ color: var(--on-surface);
+
+ h2 {
+ margin-bottom: 8px;
+ }
+
+ p {
+ color: var(--outline);
+ }
+}
+
+.interactor-detail {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.interactor-summary {
+ h1 {
+ margin: 0 0 16px;
+ font-size: 1.5rem;
+
+ a {
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .summary-fields {
+ margin: 0;
+
+ .summary-row {
+ display: flex;
+ gap: 12px;
+ padding: 6px 0;
+ border-bottom: 1px solid var(--outline, #eee);
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ dt {
+ font-weight: 600;
+ min-width: 100px;
+ flex-shrink: 0;
+ }
+
+ dd {
+ margin: 0;
+ }
+ }
+ }
+}
+
+h2 {
+ margin: 0 0 16px;
+ font-size: 1.2rem;
+}
+
+.table-wrapper {
+ overflow-x: auto;
+}
+
+.interactions-table {
+ width: 100%;
+ border-collapse: collapse;
+
+ th, td {
+ padding: 10px 14px;
+ text-align: left;
+ border-bottom: 1px solid var(--outline, #ddd);
+ }
+
+ th {
+ font-weight: 600;
+ background: var(--surface, #f5f5f5);
+ white-space: nowrap;
+ }
+
+ tbody tr:hover {
+ background: rgba(0, 0, 0, 0.02);
+ }
+
+ a {
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .entities-cell {
+ .entity-link {
+ display: block;
+ padding: 2px 0;
+
+ &:not(:last-child) {
+ border-bottom: 1px dotted var(--outline, #ddd);
+ }
+ }
+ }
+
+ .evidence-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ white-space: nowrap;
+
+ .material-symbols-rounded {
+ font-size: 16px;
+ }
+ }
+}
diff --git a/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.ts b/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.ts
new file mode 100644
index 0000000..141ce1f
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.ts
@@ -0,0 +1,226 @@
+import {Component, inject, OnInit, signal} from '@angular/core';
+import {DecimalPipe} from '@angular/common';
+import {ActivatedRoute, RouterLink} from '@angular/router';
+import {HttpClient} from '@angular/common/http';
+import {MatProgressSpinner} from '@angular/material/progress-spinner';
+import {forkJoin, of} from 'rxjs';
+import {catchError, switchMap} from 'rxjs/operators';
+import {PageLayoutComponent} from '../../../page-layout/page-layout.component';
+import {TileComponent} from '../../../reactome-components/tile/tile.component';
+import {CONTENT_SERVICE} from '../../../../../../pathway-browser/src/environments/environment';
+
+export interface CustomInteraction {
+ dbId: number;
+ identifier: string;
+ score: number;
+ evidenceCount: number;
+ url: string;
+ evidenceURL: string;
+ geneName?: string[];
+ databaseName?: string;
+ entitiesCount?: number;
+ speciesName?: string;
+ displayName?: string;
+}
+
+export interface ReactomeEntity {
+ stId: string;
+ displayName: string;
+}
+
+interface XrefMapping {
+ reference: string;
+ physicalEntities: string[];
+}
+
+interface SearchResult {
+ results: { entries: SearchEntry[] }[];
+}
+
+interface SearchEntry {
+ name: string;
+ type: string;
+ exactType: string;
+ species: string[];
+ databaseName: string;
+ referenceIdentifier: string;
+ referenceURL: string;
+}
+
+interface UniProtResponse {
+ genes?: { geneName?: { value: string }; synonyms?: { value: string }[] }[];
+ proteinDescription?: {
+ recommendedName?: { fullName?: { value: string } };
+ alternativeNames?: { fullName?: { value: string } }[];
+ };
+ organism?: { scientificName?: string };
+}
+
+@Component({
+ selector: 'app-interactor-detail',
+ standalone: true,
+ imports: [PageLayoutComponent, TileComponent, MatProgressSpinner, DecimalPipe, RouterLink],
+ templateUrl: './interactor-detail.component.html',
+ styleUrl: './interactor-detail.component.scss',
+})
+export class InteractorDetailComponent implements OnInit {
+ private route = inject(ActivatedRoute);
+ private http = inject(HttpClient);
+
+ acc = signal('');
+ interactions = signal([]);
+ loading = signal(true);
+ error = signal(false);
+
+ // Summary fields
+ displayName = signal('');
+ interactorType = signal('');
+ species = signal('');
+ synonyms = signal([]);
+ referenceURL = signal('');
+
+ // Reactome entity mapping: interactor identifier -> ReactomeEntity[]
+ entityMap = signal>({});
+
+ ngOnInit() {
+ const acc = this.route.snapshot.paramMap.get('acc');
+ if (!acc) {
+ this.loading.set(false);
+ this.error.set(true);
+ return;
+ }
+
+ this.acc.set(acc);
+ const baseAcc = acc.split('-')[0];
+
+ forkJoin({
+ interactions: this.http.get(
+ `${CONTENT_SERVICE}/interactors/static/molecule/enhanced/${encodeURIComponent(acc)}/details`
+ ).pipe(catchError(() => of(null))),
+ search: this.http.get(
+ `${CONTENT_SERVICE}/search/query`, {params: {query: acc, types: 'Interactor', cluster: 'true'}}
+ ).pipe(catchError(() => of(null))),
+ uniprot: this.http.get(
+ `https://rest.uniprot.org/uniprotkb/${encodeURIComponent(baseAcc)}.json`
+ ).pipe(catchError(() => of(null))),
+ }).pipe(
+ switchMap(({interactions, search, uniprot}) => {
+ if (!interactions) {
+ return of({interactions: null, search, uniprot, entityMap: {}});
+ }
+
+ // Fetch xrefs for each unique interactor identifier to get Reactome entities
+ const identifiers = interactions.map(i => i.identifier).filter(Boolean);
+ if (!identifiers.length) {
+ return of({interactions, search, uniprot, entityMap: {}});
+ }
+
+ const xrefRequests: Record> = {};
+ for (const id of identifiers) {
+ xrefRequests[id] = this.http.get(
+ `${CONTENT_SERVICE}/references/mapping/${encodeURIComponent(id)}/xrefs`
+ ).pipe(catchError(() => of([])));
+ }
+
+ return forkJoin(xrefRequests).pipe(
+ switchMap((xrefResults: Record) => {
+ // Collect all physical entity stIds
+ const allStIds: string[] = [];
+ const idToStIds: Record = {};
+
+ for (const [id, xrefs] of Object.entries(xrefResults)) {
+ const mappings = xrefs as XrefMapping[];
+ const stIds: string[] = [];
+ if (Array.isArray(mappings)) {
+ for (const m of mappings) {
+ if (m.physicalEntities) stIds.push(...m.physicalEntities);
+ }
+ }
+ idToStIds[id] = stIds;
+ allStIds.push(...stIds);
+ }
+
+ if (!allStIds.length) {
+ return of({interactions, search, uniprot, entityMap: {}});
+ }
+
+ // Batch fetch display names for all physical entities
+ return this.http.post<{stId: string; displayName: string}[]>(
+ `${CONTENT_SERVICE}/data/query/ids`,
+ allStIds.join(','),
+ {headers: {'Content-Type': 'text/plain'}}
+ ).pipe(
+ catchError(() => of([])),
+ switchMap((entities) => {
+ const entityLookup: Record = {};
+ if (Array.isArray(entities)) {
+ for (const e of entities) {
+ if (e?.stId) entityLookup[e.stId] = e.displayName;
+ }
+ }
+
+ const entityMap: Record = {};
+ for (const [id, stIds] of Object.entries(idToStIds)) {
+ entityMap[id] = stIds
+ .filter(stId => entityLookup[stId])
+ .map(stId => ({stId, displayName: entityLookup[stId]}));
+ }
+
+ return of({interactions, search, uniprot, entityMap});
+ })
+ );
+ })
+ );
+ })
+ ).subscribe({
+ next: ({interactions, search, uniprot, entityMap}) => {
+ if (!interactions) {
+ this.error.set(true);
+ this.loading.set(false);
+ return;
+ }
+
+ this.interactions.set(interactions);
+ this.entityMap.set(entityMap);
+
+ // Extract summary from search result
+ const entry = search?.results?.[0]?.entries?.[0];
+ if (entry) {
+ const cleanId = (entry.referenceIdentifier ?? acc).replace(/<[^>]*>/g, '');
+ this.displayName.set(`${entry.databaseName}:${cleanId} ${entry.name}`);
+ this.interactorType.set(`${entry.type}${entry.exactType && entry.exactType !== entry.type ? ' (' + entry.exactType + ')' : ''}`);
+ this.species.set(entry.species?.[0] ?? '');
+ this.referenceURL.set(entry.referenceURL ?? `https://www.uniprot.org/uniprotkb/${acc}/entry`);
+ } else {
+ this.displayName.set(`UniProt:${acc}`);
+ this.interactorType.set('Interactor');
+ this.referenceURL.set(`https://www.uniprot.org/uniprotkb/${acc}/entry`);
+ }
+
+ // Extract synonyms from UniProt
+ if (uniprot) {
+ const syns: string[] = [];
+ for (const gene of uniprot.genes ?? []) {
+ for (const syn of gene.synonyms ?? []) {
+ if (syn.value) syns.push(syn.value);
+ }
+ }
+ const protDesc = uniprot.proteinDescription;
+ for (const alt of protDesc?.alternativeNames ?? []) {
+ if (alt.fullName?.value) syns.push(alt.fullName.value);
+ }
+ if (!this.species()) {
+ this.species.set(uniprot.organism?.scientificName ?? '');
+ }
+ this.synonyms.set(syns);
+ }
+
+ this.loading.set(false);
+ },
+ error: () => {
+ this.error.set(true);
+ this.loading.set(false);
+ },
+ });
+ }
+}
diff --git a/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.html b/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.html
new file mode 100644
index 0000000..65a1504
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.html
@@ -0,0 +1,85 @@
+@if (trees().length) {
+
+
+
+
+
+ @for (root of filteredTrees(); track root.stId) {
+
+ @if (hasChildren(root)) {
+
+
+
+ @if (isExpanded(root.stId)) {
+
+
+ @for (child of root.children; track child.url || child.stId; let last = $last) {
+
+ }
+
+
+ }
+ } @else {
+
+
+
+
{{ root.name }}
+ @if (root.species && availableSpecies().length <= 1) {
+
({{ root.species }})
+ }
+
+ }
+
+ }
+
+}
+
+
+
+
+
+
+ @if (node.url && !hasChildren(node)) {
+
{{ node.name }}
+ } @else {
+
{{ node.name }}
+ }
+
+ @if (hasChildren(node)) {
+
+ @for (child of node.children; track child.url || child.stId; let childLast = $last) {
+
+ }
+
+ }
+
+
diff --git a/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.scss b/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.scss
new file mode 100644
index 0000000..9d5becf
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.scss
@@ -0,0 +1,214 @@
+:host {
+ display: block;
+ width: 100%;
+ font-family: 'Roboto', sans-serif;
+}
+
+// ── Toolbar (species selector + expand/collapse) ────────
+.toolbar {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.species-filter {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.species-label {
+ font-size: 14px;
+ color: var(--on-surface-variant);
+ white-space: nowrap;
+}
+
+.species-select {
+ font-family: 'Roboto', sans-serif;
+ font-size: 14px;
+ padding: 2px 6px;
+ border: 1px solid var(--outline-variant);
+ border-radius: 4px;
+ background: var(--surface);
+ color: var(--on-surface);
+ cursor: pointer;
+
+ &:focus {
+ outline: 2px solid var(--primary);
+ outline-offset: -1px;
+ }
+}
+
+.expand-all-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-family: 'Roboto', sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--primary);
+ white-space: nowrap;
+ padding: 4px 12px;
+ border-radius: 50px;
+ margin-left: auto;
+
+ &:hover {
+ background: color-mix(in srgb, var(--primary) 8%, transparent);
+ }
+}
+
+// ── Root entries ────────────────────────────────────────
+.tree-roots {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.root-entry {
+ display: flex;
+ flex-direction: column;
+}
+
+.root-toggle {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 4px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 2px 6px;
+ border-radius: 50px;
+ font-family: 'Roboto', sans-serif;
+ font-size: 14px;
+ line-height: 24px;
+ color: var(--on-surface);
+ text-align: left;
+
+ &:hover {
+ background: var(--surface2, color-mix(in srgb, var(--on-surface) 8%, transparent));
+ }
+}
+
+.root-leaf {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 6px 2px 28px; // align with root-toggle text (arrow width + gap)
+}
+
+.arrow-icon {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+}
+
+.type-icon {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+}
+
+.root-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+// ── Subtree (children of a root) ────────────────────────
+.subtree-container {
+ padding-left: 14px; // align with root icon
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+// ── Tree list with connector lines ──────────────────────
+ul.tree {
+ list-style: none;
+ margin: 0;
+ padding: 0 0 0 10px;
+ position: relative;
+
+ // Vertical connector line
+ &::before {
+ content: '';
+ display: block;
+ width: 0;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ border-left: 1px solid var(--outline-variant);
+ }
+}
+
+ul.tree li {
+ margin: 0;
+ padding: 0 0 0 12px;
+ position: relative;
+ font-size: 14px;
+ line-height: 22px;
+
+ // Horizontal connector line
+ &::before {
+ content: '';
+ display: block;
+ width: 10px;
+ height: 0;
+ border-top: 1px solid var(--outline-variant);
+ position: absolute;
+ top: 11px; // half of line-height
+ left: 0;
+ }
+
+ // Cut vertical line after last child
+ &.last::before {
+ background: var(--surface);
+ height: auto;
+ top: 11px;
+ bottom: 0;
+ }
+}
+
+.node-row {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 4px;
+ padding: 1px 6px 1px 2px;
+ border-radius: 50px;
+ min-height: 24px;
+
+ &:hover {
+ background: var(--surface2, color-mix(in srgb, var(--on-surface) 8%, transparent));
+ }
+}
+
+.node-name {
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ color: var(--on-surface);
+}
+
+.leaf-link {
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.node-species {
+ font-size: 12px;
+ color: var(--outline);
+ white-space: nowrap;
+ margin-left: 2px;
+}
diff --git a/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.ts b/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.ts
new file mode 100644
index 0000000..f338f3f
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/locations-tree/locations-tree.component.ts
@@ -0,0 +1,132 @@
+import {Component, computed, effect, inject, input, signal} from '@angular/core';
+import {NgTemplateOutlet} from '@angular/common';
+import {HttpClient} from '@angular/common/http';
+import {MatIcon} from '@angular/material/icon';
+import {CONTENT_SERVICE} from '../../../../../../pathway-browser/src/environments/environment';
+import {IconService} from '../../../../../../pathway-browser/src/app/services/icon.service';
+
+export interface PathwayBrowserNode {
+ stId: string;
+ name: string;
+ species: string;
+ url: string;
+ type: string;
+ diagram: boolean;
+ children?: PathwayBrowserNode[];
+}
+
+@Component({
+ selector: 'app-locations-tree',
+ standalone: true,
+ imports: [NgTemplateOutlet, MatIcon],
+ templateUrl: './locations-tree.component.html',
+ styleUrl: './locations-tree.component.scss',
+})
+export class LocationsTreeComponent {
+ private http = inject(HttpClient);
+ private iconService = inject(IconService);
+
+ id = input.required();
+ trees = signal([]);
+ expanded = signal>(new Set());
+ allExpanded = signal(false);
+ loading = signal(false);
+ selectedSpecies = signal(null);
+
+ availableSpecies = computed(() => {
+ const species = new Set();
+ for (const tree of this.trees()) {
+ if (tree.species) species.add(tree.species);
+ }
+ return [...species].sort();
+ });
+
+ filteredTrees = computed(() => {
+ const selected = this.selectedSpecies();
+ const all = this.trees();
+ if (!selected || this.availableSpecies().length <= 1) return all;
+ return all.filter(t => t.species === selected);
+ });
+
+ constructor() {
+ effect(() => {
+ const id = this.id();
+ if (id) this.fetchLocations(id);
+ });
+ }
+
+ private fetchLocations(id: string) {
+ this.loading.set(true);
+ this.expanded.set(new Set());
+ this.allExpanded.set(false);
+ this.selectedSpecies.set(null);
+ const url = `${CONTENT_SERVICE}/data/detail/${id}/locationsInPWB`;
+ this.http.get(url).subscribe({
+ next: (data) => {
+ this.trees.set(data);
+ const species = new Set();
+ for (const tree of data) {
+ if (tree.species) species.add(tree.species);
+ }
+ if (species.has('Homo sapiens')) {
+ this.selectedSpecies.set('Homo sapiens');
+ } else if (species.size > 0) {
+ this.selectedSpecies.set([...species].sort()[0]);
+ }
+ this.loading.set(false);
+ },
+ error: () => {
+ this.trees.set([]);
+ this.loading.set(false);
+ },
+ });
+ }
+
+ toggleRoot(stId: string) {
+ const current = new Set(this.expanded());
+ if (current.has(stId)) {
+ current.delete(stId);
+ } else {
+ current.add(stId);
+ }
+ this.expanded.set(current);
+ }
+
+ toggleAll() {
+ if (this.allExpanded()) {
+ this.expanded.set(new Set());
+ this.allExpanded.set(false);
+ } else {
+ const all = new Set();
+ for (const tree of this.filteredTrees()) {
+ all.add(tree.stId);
+ }
+ this.expanded.set(all);
+ this.allExpanded.set(true);
+ }
+ }
+
+ isExpanded(stId: string): boolean {
+ return this.expanded().has(stId);
+ }
+
+ hasChildren(node: PathwayBrowserNode): boolean {
+ return !!node.children?.length;
+ }
+
+ getIconName(type: string): string {
+ const icons = this.iconService.getReactomeSubjectIcons();
+ return icons[type]?.name ?? 'pathway';
+ }
+
+ onSpeciesChange(event: Event) {
+ const value = (event.target as HTMLSelectElement).value;
+ this.selectedSpecies.set(value);
+ this.expanded.set(new Set());
+ this.allExpanded.set(false);
+ }
+
+ isLastChild(parent: PathwayBrowserNode[], node: PathwayBrowserNode): boolean {
+ return parent[parent.length - 1] === node;
+ }
+}
diff --git a/projects/website-angular/src/app/content/detail/providers/detail-data-state.provider.ts b/projects/website-angular/src/app/content/detail/providers/detail-data-state.provider.ts
new file mode 100644
index 0000000..63157c2
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/providers/detail-data-state.provider.ts
@@ -0,0 +1,57 @@
+import {computed, inject, Injectable, signal} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {DataStateService} from '../../../../../../pathway-browser/src/app/services/data-state.service';
+import {SelectableObject} from '../../../../../../pathway-browser/src/app/services/event.service';
+import {DatabaseObject} from '../../../../../../pathway-browser/src/app/model/graph/database-object.model';
+import {JSOGDeserializer, JSOGObject} from '../../../../../../pathway-browser/src/app/utils/JSOGDeserializer';
+import {CONTENT_SERVICE} from '../../../../../../pathway-browser/src/environments/environment';
+import {rxResource} from '@angular/core/rxjs-interop';
+import {map, Observable, of} from 'rxjs';
+
+@Injectable()
+export class DetailDataState implements Partial {
+ private http = inject(HttpClient);
+
+ readonly selectedElement = signal(undefined);
+ readonly selectedElementLoading = signal(false);
+ readonly currentPathway = computed(() => undefined);
+ readonly hasDetail = computed(() => !!this.selectedElement());
+ readonly selectIsSummary = computed(() => false);
+ readonly selectedPathwayStId = signal(undefined);
+
+ readonly flagIdentifiers = computed(() => []);
+
+ flagResource = rxResource({
+ request: () => null,
+ loader: () => of({matches: [], interactsWith: []})
+ });
+
+ fetchEnhancedData(id: string | number | null, params?: Partial<{
+ fetchIncomingRelationships: boolean,
+ summariseReferenceEntity: boolean,
+ includeDisease: boolean
+ }>): Observable {
+ if (id === null) return of(undefined);
+ const url = `${CONTENT_SERVICE}/data/query/enhanced/v2/${id}`;
+ return this.http.get(url, {
+ params: {
+ fetchIncomingRelationships: true,
+ summariseReferenceEntity: true,
+ includeDisease: true,
+ ...params,
+ includeRef: true,
+ view: 'nested-aggregated'
+ }
+ }).pipe(map(this.flattenReferences));
+ }
+
+ flattenReferences(response: T): T {
+ const deserializer = new JSOGDeserializer();
+ return deserializer.deserialize(response as JSOGObject);
+ }
+}
+
+export const detailDataStateProvider = {
+ provide: DataStateService,
+ useClass: DetailDataState,
+};
diff --git a/projects/website-angular/src/app/content/detail/providers/detail-entity.provider.ts b/projects/website-angular/src/app/content/detail/providers/detail-entity.provider.ts
new file mode 100644
index 0000000..a40fe13
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/providers/detail-entity.provider.ts
@@ -0,0 +1,92 @@
+import {inject, Injectable, signal} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {map, Observable, of} from 'rxjs';
+import {EntityService} from '../../../../../../pathway-browser/src/app/services/entity.service';
+import {CONTENT_SERVICE} from '../../../../../../pathway-browser/src/environments/environment';
+import {PhysicalEntity} from '../../../../../../pathway-browser/src/app/model/graph/physical-entity/physical-entity.model';
+import {DatabaseObject} from '../../../../../../pathway-browser/src/app/model/graph/database-object.model';
+import {ReferenceEntity} from '../../../../../../pathway-browser/src/app/model/graph/reference-entity/reference-entity.model';
+import {DataKeys, Labels} from '../../../../../../pathway-browser/src/app/constants/constants';
+import {JSOGDeserializer, JSOGObject} from '../../../../../../pathway-browser/src/app/utils/JSOGDeserializer';
+
+@Injectable()
+export class DetailEntityService implements Partial {
+ private http = inject(HttpClient);
+
+ eventId = signal(undefined);
+ selectedElement$ = of(undefined);
+
+ _refEntities: any = {value: signal(null)};
+ refEntities = signal(null);
+
+ getOtherForms(stId: string): Observable {
+ const url = `${CONTENT_SERVICE}/data/entity/${stId}/otherForms`;
+ return this.http.get(url);
+ }
+
+ getEntityInDepth(id: string | number, depth: number): Observable {
+ const url = `${CONTENT_SERVICE}/data/entity/${id}/in-depth`;
+ return this.http.get(url, {
+ params: {
+ includeRef: true,
+ view: 'nested-aggregated',
+ attributes: 'species,compartment,referenceEntity',
+ maxDepth: depth,
+ }
+ }).pipe(map(this.flattenReferences));
+ }
+
+ getEventInDepth(id: string | number, depth: number): Observable {
+ const url = `${CONTENT_SERVICE}/data/event/${id}/in-depth`;
+ return this.http.get(url, {
+ params: {
+ includeRef: true,
+ view: 'nested-aggregated',
+ attributes: 'species,compartment',
+ maxDepth: depth,
+ }
+ }).pipe(map(this.flattenReferences));
+ }
+
+ getGroupedData(data: T[], getKey: (item: T) => string): Map {
+ const grouped = new Map();
+ const uniqueKeys = [...new Set(data.map(item => getKey(item)))];
+ uniqueKeys.forEach(key => {
+ grouped.set(key, data.filter(item => getKey(item) === key));
+ });
+ return grouped;
+ }
+
+ getTransformedExternalRef(refEntity: ReferenceEntity | undefined) {
+ if (!refEntity) return [];
+ const externalRef = {...refEntity};
+ const properties = [
+ {key: DataKeys.DISPLAY_NAME, label: Labels.EXTERNAL_REFERENCE},
+ {key: 'geneName', label: 'Gene Names'},
+ {key: 'chain', label: 'Chain'},
+ {key: 'referenceGene', label: 'Reference Genes'},
+ {key: 'referenceTranscript', label: 'Reference Transcript'}
+ ];
+ const results: { label: string, value: any }[] = [];
+ for (const property of properties) {
+ let value = (externalRef as any)[property.key];
+ if (!value) continue;
+ results.push({label: property.label || property.key, value});
+ }
+ return results;
+ }
+
+ loadRefEntities(id: string) {
+ this.eventId.set(id);
+ }
+
+ private flattenReferences(response: T): T {
+ const deserializer = new JSOGDeserializer();
+ return deserializer.deserialize(response as JSOGObject);
+ }
+}
+
+export const detailEntityProvider = {
+ provide: EntityService,
+ useClass: DetailEntityService,
+};
diff --git a/projects/website-angular/src/app/content/detail/providers/detail-figure.provider.ts b/projects/website-angular/src/app/content/detail/providers/detail-figure.provider.ts
new file mode 100644
index 0000000..f4c8e8b
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/providers/detail-figure.provider.ts
@@ -0,0 +1,17 @@
+import {Injectable, signal} from '@angular/core';
+import {FigureService} from '../../../../../../pathway-browser/src/app/details/tabs/description-tab/figure/figure.service';
+import {Figure} from '../../../../../../pathway-browser/src/app/model/graph/figure.model';
+
+@Injectable()
+export class DetailFigureService implements Partial {
+ readonly expanded = signal(undefined);
+
+ toggle(figure: Figure) {
+ this.expanded.update(prev => prev === figure ? undefined : figure);
+ }
+}
+
+export const detailFigureProvider = {
+ provide: FigureService,
+ useClass: DetailFigureService,
+};
diff --git a/projects/website-angular/src/app/content/detail/providers/detail-interactor.provider.ts b/projects/website-angular/src/app/content/detail/providers/detail-interactor.provider.ts
new file mode 100644
index 0000000..21d6cb0
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/providers/detail-interactor.provider.ts
@@ -0,0 +1,24 @@
+import {inject, Injectable, signal} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {Observable, of} from 'rxjs';
+import {InteractorService} from '../../../../../../pathway-browser/src/app/interactors/services/interactor.service';
+import {CONTENT_SERVICE} from '../../../../../../pathway-browser/src/environments/environment';
+import {CustomInteraction} from '../../../../../../pathway-browser/src/app/interactors/model/interactor.model';
+
+@Injectable()
+export class DetailInteractorService implements Partial {
+ private http = inject(HttpClient);
+
+ currentResource = signal({type: null, name: null});
+ identifiers = '';
+
+ getCustomInteractorsByAcc(acc: string): Observable {
+ const url = `${CONTENT_SERVICE}/interactors/static/molecule/enhanced/${acc}/details`;
+ return this.http.get(url);
+ }
+}
+
+export const detailInteractorProvider = {
+ provide: InteractorService,
+ useClass: DetailInteractorService,
+};
diff --git a/projects/website-angular/src/app/content/detail/providers/detail-species.provider.ts b/projects/website-angular/src/app/content/detail/providers/detail-species.provider.ts
new file mode 100644
index 0000000..3d92a84
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/providers/detail-species.provider.ts
@@ -0,0 +1,23 @@
+import {computed, Injectable, signal} from '@angular/core';
+import {SpeciesService} from '../../../../../../pathway-browser/src/app/services/species.service';
+import {Species} from '../../../../../../pathway-browser/src/app/model/graph/species.model';
+
+@Injectable()
+export class DetailSpeciesService implements Partial {
+ readonly defaultSpecies: Species = {
+ displayName: 'Homo sapiens',
+ taxId: '9606',
+ dbId: 48887,
+ shortName: 'H. sapiens',
+ abbreviation: 'HSA',
+ schemaClass: 'Species'
+ };
+
+ readonly currentSpecies = signal(this.defaultSpecies);
+ readonly allShortenSpecies = computed(() => undefined);
+}
+
+export const detailSpeciesProvider = {
+ provide: SpeciesService,
+ useClass: DetailSpeciesService,
+};
diff --git a/projects/website-angular/src/app/content/detail/providers/detail-url-state.provider.ts b/projects/website-angular/src/app/content/detail/providers/detail-url-state.provider.ts
new file mode 100644
index 0000000..2d0e48b
--- /dev/null
+++ b/projects/website-angular/src/app/content/detail/providers/detail-url-state.provider.ts
@@ -0,0 +1,67 @@
+import {inject, Injectable, signal} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {toSignal} from '@angular/core/rxjs-interop';
+import {UrlStateService, urlParam} from '../../../../../../pathway-browser/src/app/services/url-state.service';
+
+@Injectable()
+export class DetailUrlState implements Partial {
+ private route = inject(ActivatedRoute);
+
+ section = toSignal(this.route.fragment);
+ summariseDisease = urlParam(undefined, 'boolean');
+
+ select = urlParam(null, 'id');
+ flag = urlParam([], 'id');
+ path = urlParam([], 'id');
+ pathwayId = signal(undefined);
+ flagInteractors = urlParam(false, 'boolean');
+ tab = urlParam(null, 'string');
+ overlay = urlParam(null, 'string');
+ analysis = urlParam(null, 'string');
+ significance = urlParam(0.05, 'number');
+ sample = urlParam(null, 'string');
+ palette = urlParam(null, 'string');
+ filterViewMode = urlParam(undefined, 'string');
+ speciesFilter = urlParam([], 'string');
+ resourceFilter = urlParam(null, 'string');
+ includeDisease = urlParam(undefined, 'boolean');
+ includeGrouping = urlParam(undefined, 'boolean');
+ pathwayMinSizeFilter = urlParam(undefined, 'number');
+ pathwayMaxSizeFilter = urlParam(undefined, 'number');
+ minExpressionFilter = urlParam(undefined, 'number');
+ maxExpressionFilter = urlParam(undefined, 'number');
+ fdrFilter = urlParam(undefined, 'number');
+ gsaFilter = urlParam([], 'number');
+ example = urlParam(null, 'string');
+
+ readonly values = {
+ select: this.select,
+ flag: this.flag,
+ path: this.path,
+ flagInteractors: this.flagInteractors,
+ overlay: this.overlay,
+ analysis: this.analysis,
+ tab: this.tab,
+ significance: this.significance,
+ sample: this.sample,
+ palette: this.palette,
+ filterViewMode: this.filterViewMode,
+ speciesFilter: this.speciesFilter,
+ resourceFilter: this.resourceFilter,
+ includeDisease: this.includeDisease,
+ includeGrouping: this.includeGrouping,
+ pathwayMinSizeFilter: this.pathwayMinSizeFilter,
+ pathwayMaxSizeFilter: this.pathwayMaxSizeFilter,
+ minExpressionFilter: this.minExpressionFilter,
+ maxExpressionFilter: this.maxExpressionFilter,
+ fdrFilter: this.fdrFilter,
+ gsaFilter: this.gsaFilter,
+ summariseDisease: this.summariseDisease,
+ example: this.example,
+ };
+}
+
+export const detailUrlStateProvider = {
+ provide: UrlStateService,
+ useClass: DetailUrlState,
+};
diff --git a/projects/website-angular/src/app/home-page/home-latest-news/home-latest-news.component.html b/projects/website-angular/src/app/home-page/home-latest-news/home-latest-news.component.html
index dce42e6..b3c2f86 100644
--- a/projects/website-angular/src/app/home-page/home-latest-news/home-latest-news.component.html
+++ b/projects/website-angular/src/app/home-page/home-latest-news/home-latest-news.component.html
@@ -1,6 +1,6 @@
Latest News
diff --git a/projects/website-angular/src/app/home-page/home-latest-news/home-latest-news.component.ts b/projects/website-angular/src/app/home-page/home-latest-news/home-latest-news.component.ts
index d0fc6fa..250b6ec 100644
--- a/projects/website-angular/src/app/home-page/home-latest-news/home-latest-news.component.ts
+++ b/projects/website-angular/src/app/home-page/home-latest-news/home-latest-news.component.ts
@@ -1,6 +1,7 @@
import { Component, inject, Input, OnInit } from '@angular/core';
import { ArticleIndexItem } from '../../../types/article';
import { NgForOf, NgFor } from '@angular/common';
+import { RouterModule } from '@angular/router';
import formatDate from '../../../utils/formatDate';
import { ContentService } from '../../../services/content.service';
import { mapNavOptions } from '../../../utils/nav-options-mapper';
@@ -9,7 +10,7 @@ import { NavOption } from '../../../types/link';
@Component({
selector: 'app-home-latest-news',
standalone: true,
- imports: [NgForOf, NgFor],
+ imports: [NgForOf, NgFor, RouterModule],
templateUrl: './home-latest-news.component.html',
styleUrl: './home-latest-news.component.scss'
})
diff --git a/projects/website-angular/src/app/home-page/home-spotlight/home-spotlight.component.html b/projects/website-angular/src/app/home-page/home-spotlight/home-spotlight.component.html
index 24ccbb6..7def020 100644
--- a/projects/website-angular/src/app/home-page/home-spotlight/home-spotlight.component.html
+++ b/projects/website-angular/src/app/home-page/home-spotlight/home-spotlight.component.html
@@ -1,6 +1,6 @@
Reactome Research Spotlight
{{ "[" + formatD(spotLightArticle.date) + "] " + spotLightArticle.title}}
-
+
Learn More
diff --git a/projects/website-angular/src/app/home-page/home-spotlight/home-spotlight.component.ts b/projects/website-angular/src/app/home-page/home-spotlight/home-spotlight.component.ts
index 6c8fecf..2fe3993 100644
--- a/projects/website-angular/src/app/home-page/home-spotlight/home-spotlight.component.ts
+++ b/projects/website-angular/src/app/home-page/home-spotlight/home-spotlight.component.ts
@@ -1,4 +1,5 @@
import { Component, inject, Input } from '@angular/core';
+import { RouterModule } from '@angular/router';
import { ButtonComponent } from "../../reactome-components/button/button.component";
import { mapNavOptions } from '../../../utils/nav-options-mapper';
import { ArticleIndexItem } from '../../../types/article';
@@ -12,7 +13,7 @@ import { NavOption } from '../../../types/link';
@Component({
selector: 'app-home-spotlight',
standalone: true,
- imports: [ButtonComponent],
+ imports: [RouterModule, ButtonComponent],
templateUrl: './home-spotlight.component.html',
styleUrl: './home-spotlight.component.scss'
})
diff --git a/projects/website-angular/src/app/navigation-bar/navigation-bar.component.html b/projects/website-angular/src/app/navigation-bar/navigation-bar.component.html
index 7759ee2..53a6c4f 100644
--- a/projects/website-angular/src/app/navigation-bar/navigation-bar.component.html
+++ b/projects/website-angular/src/app/navigation-bar/navigation-bar.component.html
@@ -1,6 +1,6 @@