fix(explorer): address PR #503 review findings

- Extract ENTITY_SHAPE_ALIASES and classifyEntityShape into a shared
  graphEntityShape.ts utility — resolveEntityShape was duplicated with
  divergent signatures in useLoadGraph.ts and graphSceneState.ts; both
  now import from one place so aliases can never drift
- graphSceneState.resolveEntityShape falls back to classifyEntityShape
  for nodes created programmatically that bypass useLoadGraph
- Fix graphTheme.ts indentation around fullGraphStructure,
  fullGraphStructureLayer, and interaction — closing braces were at
  wrong indent levels making the nesting visually misleading
- Add comment on fullGraphStructureLayer.mode explaining it is
  intentionally "off" as a staged-rollout gate (flip to "auto" to enable
  cross-community canvas curve rendering)

Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com>
Co-authored-by: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com>
This commit is contained in:
KaifAhmad1
2026-04-27 22:21:41 +05:30
co-authored by Mohd Kaif Zohaib Hassnain
parent 82f1f6bd10
commit 438d8bc7af
4 changed files with 65 additions and 37 deletions
@@ -0,0 +1,34 @@
import type { GraphEntityShapeVariant } from "./graphTheme";
export const ENTITY_SHAPE_ALIASES: Array<[GraphEntityShapeVariant, RegExp]> = [
["biomolecule", /\b(gene|protein|enzyme|receptor|target|transcript|rna|dna|mirna|biomolecule|peptide)\b/i],
["condition", /\b(disease|condition|phenotype|symptom|disorder|syndrome|diagnosis|pathology|trait)\b/i],
["compound", /\b(drug|chemical|compound|metabolite|molecule|small[_\s-]?molecule|ligand|therapeutic|medication|substance)\b/i],
["process", /\b(pathway|process|mechanism|function|ontology|biological[_\s-]?process|cellular[_\s-]?process|program|module)\b/i],
];
export function classifyEntityShape(
nodeType?: string,
semanticGroup?: string,
content?: string,
properties?: Record<string, unknown>,
): GraphEntityShapeVariant {
const values = [
nodeType,
semanticGroup,
content,
String(properties?.type ?? ""),
String(properties?.category ?? ""),
String(properties?.label ?? ""),
]
.filter((value) => typeof value === "string" && value.trim().length > 0)
.join(" ");
for (const [shape, pattern] of ENTITY_SHAPE_ALIASES) {
if (pattern.test(values)) {
return shape;
}
}
return "entity";
}
@@ -19,6 +19,7 @@ import {
withAlpha,
zoomTierAtLeast,
} from "./graphTheme";
import { classifyEntityShape } from "./graphEntityShape";
import { computeGraphAnalyticsBase } from "./graphAnalytics";
import type {
GraphDisplayMeta,
@@ -1090,7 +1091,15 @@ export function resolveEntityShape(attrs: NodeAttributes): GraphEntityShapeVaria
return "community";
}
return attrs.entityShape || "entity";
// Prefer the shape stamped at load time; fall back to classifyEntityShape for
// nodes created programmatically that bypassed useLoadGraph.
return attrs.entityShape
|| classifyEntityShape(
attrs.nodeType,
attrs.semanticGroup,
attrs.content,
attrs.properties as Record<string, unknown> | undefined,
);
}
export function resolveEdgeVariant(state: GraphEdgeVisualState, attrs: EdgeAttributes): GraphEdgeVariant {
@@ -832,8 +832,10 @@ export const GRAPH_THEME: GraphTheme = {
bridgeMaxSize: 0.7,
structureEdgeAlpha: 0.12,
inspectionEdgeAlpha: 0.1,
},
fullGraphStructureLayer: {
},
// Staged rollout — set mode to "auto" to enable cross-community curve rendering.
// Currently "off" so the canvas overlay layer is inactive in production.
fullGraphStructureLayer: {
mode: "off",
minimumLiteralEdges: 24,
minimumCurves: 8,
@@ -843,19 +845,19 @@ export const GRAPH_THEME: GraphTheme = {
bridgeLineWidth: 0.9,
backboneLineWidth: 0.62,
curveStrength: 0.12,
},
},
interaction: {
localContextAlpha: 0.32,
hoverContextAlpha: 0.32,
selectedEdgeAlpha: 0.6,
pathEdgeAlpha: 0.76,
localContextMaxSize: 0.6,
selectedEdgeMaxSize: 1.0,
pathEdgeMaxSize: 1.4,
pathOverlayAlpha: 0.16,
},
overlays: {
},
},
interaction: {
localContextAlpha: 0.32,
hoverContextAlpha: 0.32,
selectedEdgeAlpha: 0.6,
pathEdgeAlpha: 0.76,
localContextMaxSize: 0.6,
selectedEdgeMaxSize: 1.0,
pathEdgeMaxSize: 1.4,
pathOverlayAlpha: 0.16,
},
overlays: {
hoverGlowAlpha: 0.18,
pathGlowAlpha: 0.16,
glowRadiusMultiplier: 4.8,
@@ -14,6 +14,7 @@ import {
type GraphLabelVisibilityPolicy,
type GraphNodeShapeVariant,
} from "./graphTheme";
import { classifyEntityShape } from "./graphEntityShape";
import { createGraphLoadProgress } from "./graphLoading";
import type { GraphLoadProgress, GraphLoadSummary } from "./types";
@@ -30,12 +31,6 @@ const SEMANTIC_COLOR_FIELDS = [
] as const;
const PROVENANCE_KEYS = ["source", "source_url", "pmid", "pmids", "evidence", "provenance", "confidence"] as const;
const ENTITY_SHAPE_ALIASES: Array<[GraphEntityShapeVariant, RegExp]> = [
["biomolecule", /\b(gene|protein|enzyme|receptor|target|transcript|rna|dna|mirna|biomolecule|peptide)\b/i],
["condition", /\b(disease|condition|phenotype|symptom|disorder|syndrome|diagnosis|pathology|trait)\b/i],
["compound", /\b(drug|chemical|compound|metabolite|molecule|small[_\s-]?molecule|ligand|therapeutic|medication|substance)\b/i],
["process", /\b(pathway|process|mechanism|function|ontology|biological[_\s-]?process|cellular[_\s-]?process|program|module)\b/i],
];
function getSemanticFieldValue(attributes: NodeAttributes, field: (typeof SEMANTIC_COLOR_FIELDS)[number]): string | null {
if (field === "nodeType") {
@@ -204,24 +199,12 @@ function getProvenanceCount(properties: Record<string, unknown>): number {
}
function resolveEntityShape(attributes: NodeAttributes, semanticGroup: string): GraphEntityShapeVariant {
const values = [
return classifyEntityShape(
attributes.nodeType,
semanticGroup,
attributes.content,
String(attributes.properties?.type ?? ""),
String(attributes.properties?.category ?? ""),
String(attributes.properties?.label ?? ""),
]
.filter((value) => typeof value === "string" && value.trim().length > 0)
.join(" ");
for (const [shape, pattern] of ENTITY_SHAPE_ALIASES) {
if (pattern.test(values)) {
return shape;
}
}
return "entity";
attributes.properties as Record<string, unknown> | undefined,
);
}
function resolveNodeVariantMetadata(