Skip to content

Commit 974c383

Browse files
feat: Add Cytoscape.js reaction diagram to detail pages
Extract diagramFromData() from DiagramService for reuse, create ReactionDiagramComponent that fetches reaction layout JSON and renders it client-side via Cytoscape. Integrate as a new section in DescriptionTabComponent for reaction-type entities. Fixes: handle missing endShape on INPUT connectors, missing centre on PTM attachment shapes, and bidirectional arm segment traversal for reaction diagrams from the ReactionExporter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 03a5105 commit 974c383

8 files changed

Lines changed: 545 additions & 418 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="reaction-diagram-container" #container></div>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
:host {
2+
display: block;
3+
width: 100%;
4+
}
5+
6+
.reaction-diagram-container {
7+
position: relative;
8+
width: 100%;
9+
height: 500px;
10+
border: 1px solid var(--outline-variant);
11+
border-radius: 8px;
12+
overflow: hidden;
13+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
AfterViewInit,
3+
Component,
4+
ElementRef,
5+
inject,
6+
input,
7+
OnDestroy,
8+
viewChild
9+
} from '@angular/core';
10+
import {HttpClient} from '@angular/common/http';
11+
import {Style} from 'reactome-cytoscape-style';
12+
import cytoscape from 'cytoscape';
13+
import {Diagram} from '../../../model/diagram.model';
14+
import {Graph} from '../../../model/graph.model';
15+
import {DiagramService} from '../../../services/diagram.service';
16+
import {CONTENT_SERVICE} from '../../../../environments/environment';
17+
18+
interface ReactionJson {
19+
diagram: Diagram;
20+
graph: Graph.Data;
21+
}
22+
23+
@Component({
24+
selector: 'cr-reaction-diagram',
25+
templateUrl: './reaction-diagram.component.html',
26+
styleUrl: './reaction-diagram.component.scss',
27+
})
28+
export class ReactionDiagramComponent implements AfterViewInit, OnDestroy {
29+
private http = inject(HttpClient);
30+
private diagramService = inject(DiagramService);
31+
32+
readonly stId = input.required<string>();
33+
34+
private containerRef = viewChild.required<ElementRef<HTMLDivElement>>('container');
35+
private cy?: cytoscape.Core;
36+
private reactomeStyle?: Style;
37+
38+
ngAfterViewInit() {
39+
const container = this.containerRef().nativeElement;
40+
this.reactomeStyle = new Style(container);
41+
42+
this.http.get<ReactionJson>(`${CONTENT_SERVICE}/exporter/reaction/${this.stId()}/diagram`)
43+
.subscribe(({diagram, graph}) => {
44+
// Ensure required arrays exist (reaction diagrams may omit empty arrays)
45+
diagram.links = diagram.links || [];
46+
diagram.shadows = diagram.shadows || [];
47+
diagram.compartments = diagram.compartments || [];
48+
graph.subpathways = graph.subpathways || [];
49+
50+
const elements = this.diagramService.diagramFromData(diagram, graph);
51+
52+
this.cy = cytoscape({
53+
container,
54+
elements,
55+
style: this.reactomeStyle?.getStyleSheet(),
56+
layout: {name: 'preset'},
57+
boxSelectionEnabled: false,
58+
});
59+
60+
this.reactomeStyle?.bindToCytoscape(this.cy);
61+
this.cy.fit(undefined, 20);
62+
this.cy.userZoomingEnabled(true);
63+
this.cy.userPanningEnabled(true);
64+
this.cy.autoungrabify(true);
65+
});
66+
}
67+
68+
ngOnDestroy() {
69+
this.cy?.destroy();
70+
}
71+
}

projects/pathway-browser/src/app/details/tabs/description-tab/description-tab.component.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,9 @@
287287
</div>
288288
</ng-template>
289289

290+
<ng-template #reactionDiagramTemplate>
291+
<div class="element-container">
292+
<cr-reaction-diagram [stId]="obj().stId" />
293+
</div>
294+
</ng-template>
295+

projects/pathway-browser/src/app/details/tabs/description-tab/description-tab.component.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ import {InteractorsTableComponent} from "../../common/interactors-table/interact
7676
import {
7777
LocationsTreeComponent
7878
} from "../../../../../../website-angular/src/app/content/detail/locations-tree/locations-tree.component";
79+
import {
80+
ReactionDiagramComponent
81+
} from "../../common/reaction-diagram/reaction-diagram.component";
7982

8083

8184
@Component({
@@ -108,7 +111,8 @@ import {
108111
IconComponent,
109112
RheaComponent,
110113
InteractorsTableComponent,
111-
LocationsTreeComponent
114+
LocationsTreeComponent,
115+
ReactionDiagramComponent
112116
]
113117
})
114118
export class DescriptionTabComponent implements OnDestroy {
@@ -280,6 +284,9 @@ export class DescriptionTabComponent implements OnDestroy {
280284
interactorsTemplate$ = viewChild.required<TemplateRef<any>>('interactorsTemplate');
281285
rheaTemplate$ = viewChild.required<TemplateRef<any>>('rheaTemplate');
282286
locationsTemplate$ = viewChild<TemplateRef<any>>('locationsTemplate');
287+
reactionDiagramTemplate$ = viewChild<TemplateRef<any>>('reactionDiagramTemplate');
288+
289+
readonly isReaction = computed(() => isRLE(this.obj()));
283290

284291
protected readonly Labels = Labels;
285292
protected readonly DataKeys = DataKeys;
@@ -314,6 +321,13 @@ export class DescriptionTabComponent implements OnDestroy {
314321
template: this.locationsTemplate$ as Signal<TemplateRef<any>>,
315322
isPresent: computed(() => this.showLocations())
316323
},
324+
{
325+
key: 'reactionDiagram',
326+
label: 'Reaction Diagram',
327+
manual: true,
328+
template: this.reactionDiagramTemplate$ as Signal<TemplateRef<any>>,
329+
isPresent: this.isReaction
330+
},
317331
{key: DataKeys.REFERENCE_ENTITY, label: Labels.EXTERNAL_REFERENCE, manual: true, template: this.referenceTemplate$},
318332
{key: DataKeys.SUMMARISED_ENTITIES, label: Labels.SUMMARISED_ENTITIES},
319333
{
@@ -466,6 +480,8 @@ export class DescriptionTabComponent implements OnDestroy {
466480
return obj;
467481
case 'locationsInPWB':
468482
return this.showLocations();
483+
case 'reactionDiagram':
484+
return this.isReaction();
469485
case DataKeys.PROTEIN_MARKER:
470486
return this.proteinMarkers().length + this.rnaMarkers().length > 0;
471487
case DataKeys.CATALYST_ACTIVITY:

projects/pathway-browser/src/app/model/diagram.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export interface NodeConnector {
8080
type: 'INPUT' | 'OUTPUT' | 'CATALYST' | 'ACTIVATOR' | 'INHIBITOR';
8181
segments: Segment[]
8282
stoichiometry: { value: number }
83-
endShape: { centre: Position }
83+
endShape: { centre: Position, c?: Position }
8484
isFadeOut?: boolean
8585
}
8686

0 commit comments

Comments
 (0)