diff --git a/projects/pathway-browser/src/app/details/details.component.html b/projects/pathway-browser/src/app/details/details.component.html
index c6bffad..72ed7ed 100644
--- a/projects/pathway-browser/src/app/details/details.component.html
+++ b/projects/pathway-browser/src/app/details/details.component.html
@@ -25,7 +25,7 @@
📋 Reactome Knowledgebase Details
Loading details of {{ state.select() || state.pathwayId() }}
} @else if (obj()) {
-
+
}
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 fd5321e..6b4c13d 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
@@ -144,6 +144,7 @@ export class DescriptionTabComponent implements OnDestroy {
readonly obj = input.required();
readonly analysisResult = input();
readonly showLocations = input(false);
+ readonly showReactionDiagram = input(true);
static referenceTypeToNameSuffix = new Map([
["ReferenceMolecule", ""],
@@ -325,7 +326,7 @@ export class DescriptionTabComponent implements OnDestroy {
label: 'Reaction Diagram',
manual: true,
template: this.reactionDiagramTemplate$ as Signal>,
- isPresent: this.isReaction
+ isPresent: computed(() => this.isReaction() && this.showReactionDiagram())
},
{key: DataKeys.REFERENCE_ENTITY, label: Labels.EXTERNAL_REFERENCE, manual: true, template: this.referenceTemplate$},
{key: DataKeys.SUMMARISED_ENTITIES, label: Labels.SUMMARISED_ENTITIES},
diff --git a/projects/pathway-browser/src/app/diagram/diagram.component.ts b/projects/pathway-browser/src/app/diagram/diagram.component.ts
index a838fa1..c0b33e8 100644
--- a/projects/pathway-browser/src/app/diagram/diagram.component.ts
+++ b/projects/pathway-browser/src/app/diagram/diagram.component.ts
@@ -678,7 +678,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy {
cy.batch(() => {
this.setSubPathwayVisibility(true, cy);
cy.elements().removeClass('flag');
- cy.edges('![?color]').style({'underlay-opacity': 0})
+ cy.edges().not('[?color]').style({'underlay-opacity': 0})
})
return cy.collection()
diff --git a/projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.html b/projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.html
index 77e0125..1c87338 100644
--- a/projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.html
+++ b/projects/pathway-browser/src/app/event-hierarchy/event-hierarchy.component.html
@@ -1,6 +1,6 @@
-
+
diff --git a/projects/pathway-browser/src/app/pipes/authorship-date-format.pipe.ts b/projects/pathway-browser/src/app/pipes/authorship-date-format.pipe.ts
index 168e15d..6cc9528 100644
--- a/projects/pathway-browser/src/app/pipes/authorship-date-format.pipe.ts
+++ b/projects/pathway-browser/src/app/pipes/authorship-date-format.pipe.ts
@@ -1,17 +1,21 @@
import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
-import {DatePipe} from "@angular/common";
+import {formatDate} from "@angular/common";
@Pipe({
name: 'authorshipDateFormat',
standalone: true
})
export class AuthorshipDateFormatPipe implements PipeTransform {
- constructor(private datePipe: DatePipe, @Inject(LOCALE_ID) private locale: string) {
+ constructor(@Inject(LOCALE_ID) private locale: string) {
}
transform(dateTime: string) {
if (!dateTime) return;
- return this.datePipe.transform(dateTime);
+ try {
+ return formatDate(dateTime, 'mediumDate', this.locale);
+ } catch {
+ return;
+ }
}
}
diff --git a/projects/pathway-browser/src/app/services/diagram.service.ts b/projects/pathway-browser/src/app/services/diagram.service.ts
index a3e6ecb..9c248ca 100644
--- a/projects/pathway-browser/src/app/services/diagram.service.ts
+++ b/projects/pathway-browser/src/app/services/diagram.service.ts
@@ -51,6 +51,9 @@ const squaredDist = (pos1: Position, pos2: Position) => {
const dist = (pos1: Position, pos2: Position) => Math.sqrt(squaredDist(pos1, pos2))
+const isFinitePoint = (point: Position | undefined | null): point is Position =>
+ !!point && Number.isFinite(point.x) && Number.isFinite(point.y);
+
const closestToAverage = (positions: Position[]): Position => {
const average = avg(positions);
let closest = positions[0];
@@ -562,8 +565,13 @@ export class DiagramService {
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
+ // Keep fallback direction aligned with edge source/target for connectors that have missing segment endpoints.
+ from = from ?? sourceP;
+ to = to ?? targetP;
+ if (equal(from, to)) {
+ from = sourceP;
+ to = targetP;
+ }
if (connector.type === 'CATALYST' && connector.endShape) {
to = scale(connector.endShape.centre || connector.endShape.c);
}
@@ -582,7 +590,7 @@ export class DiagramService {
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}`)
+ console.assert(Math.abs(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) {
@@ -595,7 +603,11 @@ export class DiagramService {
}
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;
+ replacement = node.connectors.find(otherConnector => {
+ if (otherConnector === connector || !otherConnector.isFadeOut) return false;
+ const otherEdge = idToEdges.get(otherConnector.edgeId);
+ return samePoint(otherEdge?.position, reaction.position);
+ })?.edgeId;
// console.log("Reaction edge", replacement)
// Second case: different nodes are used between special and normal context
@@ -603,6 +615,7 @@ export class DiagramService {
// console.log("Reaction edge", replacement)
}
+ const relativeSegments = this.getRelativeSegmentsData(relatives);
const edge: cytoscape.EdgeDefinition = {
data: {
id: this.getEdgeId(source, connector, target, edgeIds),
@@ -610,10 +623,8 @@ export class DiagramService {
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),
+ ...relativeSegments,
+ ...this.getEndpointData(sourceP, targetP, from, to),
pathway: eventIdToSubPathwayId.get(reaction.reactomeId),
reactomeId: reaction.reactomeId,
reactionId: reaction.id,
@@ -645,9 +656,14 @@ export class DiagramService {
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
+ if (equal(from, to)) {
+ from = sourceP;
+ to = targetP;
+ }
// points = addRoundness(from, to, points);
const relatives = this.absoluteToRelative(from, to, points);
+ const relativeSegments = this.getRelativeSegmentsData(relatives);
const classes = [...this.linkClassMap.get(link.renderableClass)!];
if (link.isDisease) classes.push('disease');
@@ -660,10 +676,8 @@ export class DiagramService {
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),
+ ...relativeSegments,
+ ...this.getEndpointData(sourceP, targetP, from, to),
isFadeOut: link.isFadeOut,
isBackground: isBackground
},
@@ -742,8 +756,38 @@ export class DiagramService {
}
}
- private endpoint(source: Position, point: Position): string {
- return `${point.x - source.x} ${point.y - source.y}`
+ private endpoint(source: Position, point: Position): string | undefined {
+ const dx = point.x - source.x;
+ const dy = point.y - source.y;
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) return undefined;
+ return `${dx} ${dy}`
+ }
+
+ private getEndpointData(source: Position, target: Position, from: Position, to: Position): Partial<{ sourceEndpoint: string, targetEndpoint: string }> {
+ if (!isFinitePoint(source) || !isFinitePoint(target) || !isFinitePoint(from) || !isFinitePoint(to)) {
+ return {};
+ }
+ if (equal(source, target)) {
+ return {};
+ }
+ const sourceEndpoint = this.endpoint(source, from);
+ const targetEndpoint = this.endpoint(target, to);
+ if (!sourceEndpoint || !targetEndpoint) {
+ return {};
+ }
+ return {sourceEndpoint, targetEndpoint};
+ }
+
+ private getRelativeSegmentsData(relatives: RelativePosition): Partial<{weights: string, distances: string}> {
+ const weights = relatives.weights.filter(value => Number.isFinite(value));
+ const distances = relatives.distances.filter(value => Number.isFinite(value));
+ if (weights.length === 0 || distances.length === 0 || weights.length !== distances.length) {
+ return {};
+ }
+ return {
+ weights: weights.join(" "),
+ distances: distances.join(" "),
+ };
}
@@ -761,19 +805,26 @@ export class DiagramService {
const relatives: RelativePosition = {distances: [], weights: []};
if (toConvert.length === 0) return relatives;
+ if (!isFinitePoint(source) || !isFinitePoint(target) || equal(source, target)) return relatives;
+
const mainVector = array([target.x - source.x, target.y - source.y]); // Edge vector
+ if (mainVector.x === 0 && mainVector.y === 0) return relatives;
const orthoVector = array([-mainVector.y, mainVector.x]) // Perpendicular vector
.normalize(); //Normalized to have the distance expressed in pixels https://math.stackexchange.com/a/413235/683621
- let transform = array([
+ const transform = array([
[mainVector.x, mainVector.y],
[orthoVector.x, orthoVector.y],
]).inv(); // Should always be invertible if the ortho vector is indeed perpendicular
for (let coord of toConvert) {
+ if (!isFinitePoint(coord)) continue;
const absolute = array([[coord.x - source.x, coord.y - source.y]]);
const relative = absolute.multiply(transform);
- relatives.weights.push(relative.get(0, 0))
- relatives.distances.push(relative.get(0, 1))
+ const weight = relative.get(0, 0);
+ const distance = relative.get(0, 1);
+ if (!Number.isFinite(weight) || !Number.isFinite(distance)) continue;
+ relatives.weights.push(weight)
+ relatives.distances.push(distance)
}
return relatives;
}
@@ -869,7 +920,8 @@ export class DiagramService {
}
}
-function samePoint(p1: Position, p2: Position) {
+function samePoint(p1?: Position | null, p2?: Position | null) {
+ if (!p1 || !p2) return false;
return p1.x === p2.x && p1.y === p2.y
}